Changelog

Everything we've shipped.

Cards402 is a platform, so every change matters. This page is updated the same day a change lands in production. Security-sensitive fixes are disclosed here after the patch is out. Breaking API changes are always announced 30 days before they take effect.

Defense-in-depth audit — ~95 fixes across backend, SDK, and web

securityfixinfra

A two-day adversarial audit covering every source file in the backend (42 of 46 files modified) and every security-critical SDK module. Backend test suite doubled from ~488 to 1,038; SDK stays at 114 (existing coverage was strong). The audit found and fixed bugs across the full severity spectrum — from treasury-loss races to deploy-typo DoS to silent audit-row loss. Highlights by category: Treasury safety: (1) The reconciler's hard-fail UPDATE was unconditional — a concurrent vcc-callback flipping an order to 'delivered' could be overwritten to 'failed', and scheduleRefund would send the agent both the card AND a refund. Fixed with atomic WHERE status guards + changes===0 race detection at both reconciler sites. (2) USDC refunds ignored the excess_usdc column — an agent overpaying by $0.50 on a $10 order had the excess tracked but the refund sent only $10. Fixed with BigInt stroop-precision sum. (3) A corrupt/empty order.amount_usdc parsed as 0 via toStroops, causing any positive on-chain payment to be accepted as 'overpayment of $0'. Added strict positive-amount validation before the comparison. Auth & identity: (4) API key expires_at checked via `new Date(x) < new Date()` — NaN from a corrupt date evaluates to false, so the key never expires. Fail-closed to expired + loud ops log. (5) Array-valued Authorization headers crashed requireAuth, /auth/me, and /auth/logout with 500 instead of 401 (arrays don't have .replace). Added coercion + trim at all three sites. (6) Platform-owner helper returned true when both sides trimmed to empty — silent privilege escalation from whitespace-only inputs. Observability: (7) requireCardReveal (the PAN/CVV gate) had zero forensic trace on denied attempts — hostile probing was invisible. Every branch now emits a dedup'd bizEvent. (8) Audit log recordAudit silently dropped rows when callers passed array-valued headers — moved coercion into the library boundary so every present and future caller is protected. (9) Unknown agent_state values silently rendered as 'Minted' — now surface as 'Unknown' with a one-shot warn. Resilience: (10) Circuit breakers in both vcc-client and the webhook layer had an in-flight-success race that could reopen a tripped breaker during cooldown. Fixed at both sites. (11) The Soroban watcher's dispatch-retry map used FIFO eviction that could reset an actively-poisoning event's counter — defeating the poison-pill breakout. Raised cap to 8192 and eliminated eviction. (12) The background job loop ran 12 sub-jobs under one try/catch — one failure blocked all subsequent jobs forever. Isolated each sub-job. (13) Missing PRAGMA busy_timeout meant concurrent SQLite writers got instant SQLITE_BUSY → 500. SDK: (14) SSE parser only handled \n\n delimiters — a proxy rewriting to \r\n caused unbounded buffer growth → OOM. Added CRLF normalization + 1 MB buffer cap. (15) HTTPS enforcement on Cards402Client only ran when baseUrl came from config — explicit callers (MCP, CLI, OWS) bypassed it entirely. (16) Testnet Horizon fallbacks were hardcoded to mainnet in submitSorobanTx — testnet agents got false 'dropped' signals and risked double-submits. (17) `cards402 wallet trustline` didn't pass the passphrase from config — agents with encrypted vaults couldn't open a USDC trustline via the CLI. Environment & config: (18) Stellar strkey validation only checked the first character — 'S' passed for STELLAR_XLM_SECRET. Now enforces 56-char base32. (19) INTERNAL_EMAILS and CORS_ORIGINS were opaque comma-separated strings with no per-entry validation at boot. (20) Windows agents skipped all config security checks including the file-size cap. Web: (21) Mobile nav sheet only opened ~48px tall — the sticky nav's backdrop-filter created a containing block for position:fixed descendants. Portaled to document.body. (22) More-menu dropdown closed on click after opening on hover. Click now only opens. Full changelog with per-finding rationale: github.com/cards402/cards402 CHANGELOG.md [Unreleased] section.

Security + correctness audit — 18 fixes across backend and SDK

securityfixinfra

An end-to-end audit of the security-critical primitives and the agent-facing SDK, covering the backend webhook layer, auth middleware, HMAC signing, card-at-rest sealing, SSRF guard, policy engine, error sanitiser, and every CLI surface operators touch. Eighteen commits shipped. Highlights: (1) Outbound HTTPS webhooks were silently failing in production — the DNS-pinning code rewrote the URL hostname to the resolved IP, which broke TLS certificate verification for every webhook endpoint with a CA-issued cert. Dropped the rewrite, kept the SSRF validation; the DNS-rebinding window is back but narrow and well-understood. (2) Auth middleware now blocks suspended keys at every /v1/* route — previously `suspended` was only enforced at order-creation time, so read endpoints let flagged agents straight through. (3) SSRF guard was missing IPv4-mapped IPv6, fe80::/10 past fe80, IPv4 multicast (224/4), reserved ranges, broadcast, and the RFC 5737 doc prefixes. Rewrote on Node's built-in net.BlockList with 19 regression tests. (4) Card-vault sealed blobs are now shape-validated (truncated rows no longer throw an opaque TypeError), and per-field decrypt errors are labelled so ops can pinpoint which order and which column is corrupt. (5) Policy engine fails closed on NaN/negative amounts and corrupt numeric policy columns — previously both silently disabled rules. (6) `/vcc-callback` rate limiter was a single global bucket; bucket by IP so attackers can't starve legitimate callbacks. (7) `/auth/me` no longer shares a rate-limit budget with `/auth/login`, so dashboard browsing from NAT'd networks stops hitting 429s. (8) SDK sweep: CLI amount bounds match backend ($0.01 / $10,000), `--asset auto` added to `purchase` + MCP, config writes are now atomic with re-tightened perms, `waitForCard` no longer doubles its timeout on SSE fallback, onboard refuses to silently overwrite existing credentials. (9) Enhanced /status endpoint returns live 24h delivery + watcher freshness, and a real /status page + status.cards402.com subdomain route. Backend suite went from ~200 tests to 336; new regression guards shipped alongside every fix.

vsdk@0.4.7

SDK 0.4.7 — self-updating CLI warning + @latest everywhere in the docs

featurefix

The CLI now prints a one-line stderr warning on every invocation if your installed version is older than the latest on npm. The check fires in the background with a 2s fetch timeout, caches its result in ~/.cards402/version-check.json for 24h, and never blocks or fails the actual command — stdout is untouched so scripts parsing CLI output are unaffected. Every operator-facing install snippet across skill.md, the quickstart, the landing page, the dashboard claim-code drawer, the developer page, llms.txt, and the examples README now pins @latest on npx so each invocation re-resolves against the registry instead of serving a stale cached version. The practical effect: when we ship an SDK patch release (like 0.4.6 which unblocked USDC purchases), agents onboarded under the new instructions pick it up on their next run instead of having to wait for the npx cache to expire or for the operator to manually nuke it.

vsdk@0.4.6

SDK 0.4.6 — stop stranding agents on failed Soroban transactions

fixapi

Two bugs in the SDK's Soroban submit path were combining to leave agents polling forever on orders whose payment never actually landed. (1) FAILED statuses from getTransaction were swallowed by a catch-block intended for XDR version errors, so the poll loop ran to the 120s deadline. (2) The deadline-timeout branch attached a txHash to its error unconditionally, which purchaseCardOWS treated as "envelope may still land" — so transactions that had already failed on-chain or never made it into a ledger still triggered the waitForCard fall-through. Fixed by re-raising terminal throws out of the poll loop and only attaching txHash when Horizon itself is unreachable. Package exports now declare both `import` and `require` so CommonJS consumers don't trip on ERR_PACKAGE_PATH_NOT_EXPORTED. Upgrade with `npx -y cards402@latest` or `npm i cards402@latest`.

New post: claim codes — credentials that never touch the transcript

feature

Security-architecture post on why Cards402 onboards agents with single-use claim codes instead of raw API keys. The failure mode (keys in LLM chat transcripts), the three options we considered (OAuth, env-only instructions, one-time exchange tokens), why we picked the last one, and what else building the primitive unlocked.

New post: why SSE beats polling for agent-facing APIs

feature

Why the Cards402 SDK defaults to Server-Sent Events for order state, why we kept polling as an automatic fallback, and the operational details (keepalive interval, nginx buffering, terminal close) that make SSE reliable in practice when your clients are long-lived processes instead of browsers.

New post: non-custodial card issuance on Soroban

feature

Architectural walk-through of the Cards402 receiver contract, the Soroban event watcher, and the refund story. Why agents pay a contract we can't drain, what we gave up on the latency side, and why the trade was worth it for a payment platform aimed at autonomous agents.

First blog post: anatomy of a Cards402 order

feature

New /blog index plus the first real post — a walk-through of the median 33-second path from purchaseCardOWS() through Stellar, the watcher, Stage 1/2 fulfilment, and the SSE stream, with the P50 timings we see on mainnet today. Cross-posted to the changelog RSS feed.

Site overhaul: pricing, legal, security, careers

feature

New marketing + legal surface. Pricing page with the full Pathward fee breakdown, dedicated Security, Company, Careers, Press, and Affiliate pages. Plain-English cardholder agreement summary. Sitemap, robots, and structured data for search.

v1.2.0

Docs redesign & brand polish

feature

Docs page rewritten onto the Fraunces/IBM Plex type system with editorial section scaffolding. New favicon, Cards402 casing swept across every user-visible surface, notification tray with empty state, login form now submits on Enter.

Email logo visibility on dark background

fix

Transactional emails now load a pre-tinted /logo-light.svg variant so the wordmark renders on the dark email template instead of collapsing to an invisible black mask.

Dashboard polish: overflow fixes + microinteractions

feature

KPI tile hover lift, row accent on table hover, horizontal scroll hint on borderless cards, theme toggle hides on iPhone-SE-class viewports.

Hero card with parallax tilt

feature

New hero section with a lerped-cursor parallax-tilted virtual card and full load-in choreography. Wrap entry, outline draw, glow pulse, fill, content lift, float idle.

v1.1.0

Cards402 brand refresh

feature

New wordmark rendered via CSS mask for theme-aware colouring. Fraunces display + IBM Plex Sans body + IBM Plex Mono data. Darker canvas, muted mint accent, grain overlay, radial glows.

v1.0.0

Architecture v2 — agents pay VCC directly

apisecurity

Non-custodial payment flow: agents now sign and submit Soroban contract invocations directly to the receiver contract. Cards402 proxies the 402 response and observes on-chain events. No funds held in intermediate custody.

First live order on mainnet

infra

First end-to-end live order on Stellar mainnet. $0.02 to verify the pipeline, ~33s from payment to PAN. Five watcher bugs found and fixed in the process.

SSE phase stream + waitForCard()

apifeature

New /orders/:id/stream endpoint pushing order state over Server-Sent Events with a 15-second keepalive comment. SDK waitForCard() defaults to SSE with automatic polling fallback.

Claim-code onboarding

featuresecurity

Single-use claim codes replace raw API keys in the agent onboarding flow. Operators mint a claim, share it once, the agent exchanges it for a real key on first boot. Credentials never hit the LLM transcript.