All notable changes to the x402 LLM Gateway project.
-
A browser wallet can now complete a sign-in: challenge verification accepts the SEP-53 signature wallets actually produce.
verifyChallengein@x402/walletverified a raw signature over the challenge bytes — the shape this repository's own SDK/CLI signer produces — while every browser wallet signs a message per SEP-53, assha256("Stellar Signed Message:\n" + challenge). Measured against a live gateway: a raw signature for a keypair verified, the same keypair's SEP-53 signature for the same challenge returned401 Invalid signature, so no browser wallet could log in even once the page reached one. Both shapes are now accepted, and each is a fixed function of the server-issued challenge, so a signature captured for another challenge — or a transaction signature — still fails. The alternative, trusting a signature payload the client hands back, would make a captured signature replayable.packages/wallet/src/index.spec.tspins the SEP-53 preimage and the four rejection cases: another challenge, another keypair, a non-base64 signature and a transaction signature used as a login signature. -
The dashboard login page reaches wallets through their SDKs, not injected globals. It read
window.freighterApi,window.xBullSDKandwindow.albedowhile no package was loaded to define any of them, so detection always failed and the page never made a single request to the gateway: every click ended in "wallet not found" while both API calls behind it worked.window.freighterApiexists only when the library is loaded from a CDN<script>tag andwindow.xBullSDKonly inside the xBull extension's own injected context, so for an app built with a bundler the SDKs are the only supported path. Freighter and xBull now connect and sign through@stellar/freighter-apiand@creit.tech/xbull-wallet-connect, imported lazily so they load only on a click.Albedo is removed rather than left as a button that cannot work.
albedo.signMessagereturns a signature over a message Albedo derives from the public key and the original text, and that derivation is not published, so the gateway cannot check it; verifying the returned signature against the client-supplied bytes would make a captured signature replayable as a login. The README and roadmap entries that listed Albedo as supported are corrected. -
Dashboard sign-in no longer depends on a cross-site cookie. With
NEXT_PUBLIC_GATEWAY_SAME_ORIGIN=truethe page calls/api/v1/*on its own origin and the rewrite inapps/dashboard/next.config.jsproxies that to the gateway, so the session cookie it sets is bound to the dashboard's host — first-party. Previously a page on*.vercel.appcalled*.up.railway.appdirectly, which makes the cookie third-party: Safari's ITP and Chrome's third-party-cookie limits are free to drop it, so sign-in could appear to succeed and then not stick. Verified against a live deployment — a real signed challenge through the dashboard origin returns a host-onlySet-Cookie(HttpOnly,Secure, noDomain) that authenticates the next call.Same-origin mode still fails closed: a build with the flag on and no
NEXT_PUBLIC_GATEWAY_URLbreaks the build rather than shipping a dashboard whose every/api/v1request 404s, and a production runtime without either reports a configuration error instead of calling anything. -
TRUST_PROXY=trueno longer crashes the gateway at boot.parseTrustProxy()mapped''/false/0tofalseand numeric strings to numbers, then passed everything else through as a string — sotruereachedapp.set('trust proxy', 'true')andproxy-addrthrew a bareTypeError: invalid IP address: trueduring bootstrap, naming neither the variable nor the accepted forms. A Railway deploy failed its healthcheck on exactly this.truenow maps to the boolean Express expects, and a value Express cannot compile (a typo,*, a hostname such aslocalhost) fails fast with an actionable message instead of a cryptic boot crash; every other documented form (hop counts,loopback/linklocal/uniquelocal, IP and CIDR lists) is validated against the same rulesproxy-addrapplies. -
The production smoke check no longer leaves the working tree dirty. Its report is tracked at
docs/evidence/production-smoke.jsonand embedded a wall-clockrunAt, so running the check always produced a one-line timestamp diff — noise that had to be committed or discarded by hand every time. The report is now rewritten only when the findings differ (a changed URL, a new check, a changed result), so itsrunAtreads as "the run that produced this report"; the checks themselves are re-run on every invocation either way. CI setsEVIDENCE_ALWAYS=1, so a run summary never shows a report dated to an earlier run. -
The Vercel deploy workflow deploys from the repository root, and the Git-integration failure mode behind pushes that deploy nothing is written down. The workflow ran the Vercel CLI in
apps/dashboard, which uploads that directory alone — no rootpnpm-lock.yaml, nopnpm-workspace.yaml— while the project's build command ispnpm install --frozen-lockfilefollowed bypnpm exec next build. Root Directory is applied by Vercel server-side at build time, so uploading the repository root is what keeps the workspace intact; avercel deployfrom the root builds and deploys this dashboard correctly (measured with the project's own token, not assumed). The README carried the samevercel --prod --cwd apps/dashboardadvice and is corrected. Separately,DEPLOYMENT.md§2.5 now records why a push can produce no deployment at all: a project resolves its repository throughlink.gitCredentialId, the credential created for the GitHub App installation on whichever account owned the repository when it was connected, so transferring the repository to another owner silently invalidates it —git_info_failon a manual git deployment andrepo_not_foundon reconnect, while the project itself looks healthy. Renaming the same account is harmless, which is what makes the distinction easy to miss. The section gives the two API calls that confirm it and the two browser steps that fix it. -
On-chain settlement works for the first time:
@stellar/stellar-sdk12.3.0→16.3.0(LTS). The pinned client predated protocol 23, which Testnet now runs, so it could not decode a submitted transaction's meta: every Soroban write failed withBad union switch: 4while the failure was swallowed by best-efforttry/catchblocks that log and continue. Two earlier defects had to be fixed in sequence before this one became visible — the signing-API misuse and the null source account — which is why "the payout leg fails" was never a single bug. Seedocs/VERIFICATION.md§8 #10–#11.signAuthEntriestakesaddressrather thanpublicKeyin SDK 16, the signer callbacks resolve to{ signedTxXdr }/{ signedAuthEntry }, and the SDK's@noble/hashes2.x dependency is ESM-only — Node canrequire()it but Jest cannot, so the affected Jest projects now transform those modules and enableallowJsfor their spec tsconfigs.Verified end to end on Testnet: the multisig payout leg now deploys, funds, proposes, approves, executes the USDC transfer on-chain and is recorded as
executedwith its settlement hash. Evidence indocs/VERIFICATION.md§6. -
Payout execution is read back from the contract instead of inferred from a return value.
multisig.approve(..)is declared-> (), but the client didexecuted = Boolean(tx.result), so a threshold-1 payout that had already moved funds was recorded as merelyapprovedwith noexecutedAt. The unit test encoded the same wrong assumption (a mocked boolean result) and passed regardless; it now covers the real contract shape and fails closed if the execution state cannot be confirmed. -
PayoutProposal.txHashis now populated. Both the admin and cron payout paths persist the submitted transaction hash — the settling transaction when the payout executes — so a payout can be traced from the database to Horizon. -
The live payout harness is re-runnable. A fixed seed
Payment.txHashtripped a unique constraint on the second run, previous proposals reserved the seeded revenue so a repeat run found nothing payable, andBigIntSAC balances broke evidence serialization. The seed is now upserted and sized to leave exactly 1 USDC payable, and the balance reader simulates from a real account rather than trying to load a contract address as a Horizon account (which always returned400and silently made the payout assertions vacuous). -
The DAST job had never scanned anything. The OWASP ZAP step handed every file-writing option an absolute path, but ZAP requires that directory to be mounted at
/zap/wrk; without the mount it printed its own usage text and exited3before scanning, whichcontinue-on-error: truereported as success — so the DAST gate'sFAIL-NEW 0 / WARN-NEW 0 / PASS 66was never a measurement. The reports directory is now mounted, the step fails loudly when the scan does not run, and a new assertion requires both reports to exist (ZAP exits0even when its own report job fails). A second defect surfaced only once the gate actually ran: ZAP executes as uid1000inside the image while the runner's uid differs, so the runner-owned755mount was not writable and the scan died with[Errno 13] Permission denied— the mount is now0777. Reproduced locally by running the image'szapuser against a755directory owned by another uid (Permission denied, writable after0777), then verified end to end against the local gateway: both reports written,FAIL-NEW 0 / WARN-NEW 0 / PASS 66. -
The SSRF guard no longer classifies non-public IPv6 as public.
isPublicIpmatched IPv6 by string prefix and returnedtruefor everything it did not recognise, so::— the unspecified address, which connects to localhost on Linux — plusff00::/8multicast, IPv4-compatible::a.b.c.d, NAT6464:ff9b::/96, 6to42002::/16(which carries an arbitrary IPv4 in groups 1–2) and Teredo2001:0::/32all passed as safe webhook/upstream targets. A route or webhook hostname with an AAAA record of::therefore reached the gateway's own loopback with no DNS rebinding required. Addresses are now expanded to eight groups and admitted only as global unicast (2000::/3) with no embedded private IPv4; nine regression cases pin it. The remaining check-then-fetchrebinding window is documented, not claimed fixed — seeSECURITY.md"Known Residual Risks" #10. -
Running the Testnet journey no longer deletes the escrow leg's evidence.
testnet-journey.tswrotedocs/evidence/testnet-journey.jsonwithwriteFileSyncwhile the escrow and payout legs append their sections to the same file, so re-running the journey dropped the wholeescrowsection thatdocs/VERIFICATION.md§6 cites. It now merges ({...existing, ...evidence}) like the other two writers; verified by re-running the journey and confirming the 10:30 escrow section survived the 13:21 run. -
Removed two files' "All rights reserved" copyright headers.
payments.service.tsand its spec carried// Author: RawNuke/// Copyright (c) 2026 RawNuke. All rights reserved.— the only two files in the tree with such a notice, and incompatible with the MITLICENSEthey ship under. -
The email notification channel is no longer claimed anywhere. It was already deleted as dead code (no registered handler, inert
EMAIL_*/SMTP_*config, no recipient model), but the claim survived in the README delivery matrix (Webhook + email delivery✅),.github/WAVE8_ISSUES.md(Issue 9 with every acceptance criterion ticked and a✅ closedstatus),GRANT_SUBMISSION.md("18 implemented") andpackages/types(NotificationChannelstill listedemail). All four are corrected, and the three e2e mocks that returned a fabricated['email']channel now return a channel that exists. -
A release tag no longer goes green having published nothing.
deploy.ymlwarned and exited0whenDOCKER_USERNAMEwas unset, so av*tag implied release images that did not exist. The job now fails with the missing secrets in the job summary, using the same explicitALLOW_DEPLOY_SKIP=truerepository-variable opt-out as the Vercel workflow — the false-green deploy pattern documented indocs/VERIFICATION.md§7. -
The contract size gate now runs everywhere an artifact is built or shipped, not only in CI.
docs/VERIFICATION.mddocumentedpnpm build:contractsas "wasm + size gate", butscripts/build-contracts.shbuilt and reported success without checking anything, andscripts/deploy-contracts.shuploaded whatever it had just built — so the 64 KiB Soroban limit was enforced by one CI step (via the GNU-onlystat -c%s) and an oversized artifact could reach the network before anything objected. There is now a single implementation,scripts/check-contract-sizes.sh, invoked by the CIcontractsjob, bypnpm build:contracts, and by the deploy script before each upload. It useswc -c(POSIX, so macOS contributors get the same gate), fails when an artifact is missing instead of passing vacuously, and discovers it undertarget/*/release/becausestellar contract buildhas writtenwasm32v1-nonesince CLI 23 while CI'scargo buildwriteswasm32-unknown-unknown. Nine fixture cases pin the boundary (65,536 bytes passes, 65,537 fails), oversize rejection, the missing-artifact failure and the limit override. -
The README no longer presents
CORS_ORIGINSas mandatory for the dashboard. It said the dashboard "calls the gateway directly from the browser, so two settings are mandatory", listingNEXT_PUBLIC_GATEWAY_URLand the gateway'sCORS_ORIGINS. Only the first is unconditional: withNEXT_PUBLIC_GATEWAY_SAME_ORIGIN=true— the mode the same README's deployment guide recommends — the browser calls/api/v1/*on the dashboard's own origin and the rewrite proxies it server-to-server, so no request crosses an origin and the dashboard does not need to be listed inCORS_ORIGINS. Following the README literally meant editing a gateway variable to fix a symptom the dashboard did not have. The two behaviours are now stated as the modes they are, including why the cookie's first-party or third-party nature follows from the same flag.
pnpm vercel:git-link-check— one command for the diagnosis that otherwise takes an afternoon.scripts/vercel-git-link-check.shreports whether the Vercel project's Git link can still read the repository (§2.5): it compareslink.repoOwnerIdwith the repository's currentowner.idfrom GitHub, checks the repo id, and reports the age of the newestsource: gitdeployment, exiting non-zero — with the two fix steps — when the link cannot be trusted. It distinguishes the harmless case (a rename: same owner id, stale name → warning) from the fatal one (a transfer: different owner id), which is the distinction that cost the investigation.GITHUB_TOKENis needed only for a private repository, andEVIDENCE_OUTwrites the JSON report. Unlike the production smoke check it has no default evidence file, so running it never dirties the working tree.
-
The dashboard login flow is covered for the first time. It was the one page where a broken wallet integration was invisible from the outside: it read
window.freighterApi/window.xBullSDK, nothing defined them, and every click ended in "wallet not found" while the whole auth API behind it worked — a defect the dashboard's specs could not see, because all of them test plainlib/*functions.apps/dashboard/src/app/login/page.spec.tsxnow renders the real page with React Testing Library and mocks the two wallet SDKs,next/navigationand@/lib/api, so the assertions are about what the page sends: the challenge request carries the address the wallet returned, the challenge that gets signed is the gateway's rather than a locally built one, both Freighter signature shapes (a v4 base64 string and a v3 byte array) reach the wire base64-encoded, a rejected verification renders an actionable error and leaves the buttons usable, sign-in completes from the session cookie alone when the gateway returns no token, and the dev-mode fallback produces adev-sig-payload only whenNEXT_PUBLIC_DEV_WALLETis armed. A mutation check pins the value of the suite: restoring the old undefined-global bail-out fails 11 of its 12 cases.@testing-library/reactand@testing-library/domare new dashboard devDependencies, andapps/dashboard/tsconfig.spec.jsonis new because the app tsconfig setsjsx: "preserve"for Next, which ts-jest cannot emit —jest.config.tsnow points ts-jest at the spec config.
-
Contract initialization is now atomic (C11 fixed).
stellar contract deployand a separateinitcall are two different transactions, so anyone could previously initialize a freshly deployed contract first with their ownadmin(payment-verifier, credit-escrow) or their own single-signer set (multisig) — taking ownership of a contract the deployer had just created. The re-init guard in each contract covered the second call, not the first. All three contracts now initialize in a Soroban__constructor, which executes inside the deploy transaction, and theinitentry point was removed so no post-deploy initialization path exists at all. This also removes the "deployed but never initialized" failure mode: a contract whose constructor rejects its arguments now fails the deploy outright.Note that the obvious fix —
admin.require_auth()oninit— would not have worked: the address is a caller-supplied parameter, so an attacker names and signs their own, andrequire_authonly proves control of the address the caller supplied, never that they are the deployer.This is a deploy-ABI change:
scripts/deploy-contracts.shnow passes each contract's constructor arguments tostellar contract deploy(-- <args>) instead of callinginitafterwards, and all three contract test suites were migrated toenv.register(Contract, (<constructor args>)). Because there is no on-chain state to migrate, the next deployment simply uses the new path. All 111 contract tests pass (29 / 46 / 36) and the WASM artifacts build to 7–9 KiB against the 64 KiB deploy limit. SeeSECURITY.md(residual risk 9),THREAT-MODEL.md(C2/C11) andMAINNET_READINESS.md§5/§6. -
SSRF redirect bypass closed. The public-IP validation for webhook URLs and upstream LLM URLs was performed on the initial destination, but no
fetchin the repository setredirect, so undici's default behaviour followed a3xxto internal infrastructure (e.g.169.254.169.254) after the check had passed — and for routes the redirect body is returned to the caller. Both proxy fetches and both webhook deliveries now useredirect: 'error'. -
Webhook delivery timeout: the plain
WebhookNotificationHandler.send()path had noAbortSignal, so a receiver that never responded could hold the/webhooks/testrequest open unboundedly; both delivery paths now time out after 10 s. -
Dependency advisories cleared and the scan is now a gate.
js-yaml4.3.1 → 5.4.1 andsmol-toml1.6.1 → 1.8.0 (newpnpm-workspace.yamloverride floors); inpython/uv.lockurllib32.6.3 → 2.7.0,langchain-core0.3.86 → 1.6.3,langsmith0.4.37 → 0.12.4,requests2.32.5 → 2.34.2,orjson3.11.5 → 3.12.0 andpytest8.4.2 → 9.1.1. The PyPI packages were each pinned twice — a patched pin for 3.10+ and an unpatched one for the>=3.9branch — sopython/pyproject.tomlnow requires Python>=3.10(3.9 is EOL with no patched releases) and the lock resolves as a single branch; 47/47 SDK tests pass on the new pins. osv-scanner: 28 → 15 advisories; Trivy fs: 0 HIGH/CRITICAL. -
The osv-scanner CI job no longer swallows its exit code. Findings that are not explicitly reviewed in the new
.osv-scanner.tomlnow fail the build, and every exception there carries a reason and anignoreUntilexpiry. The remaining 15 are the advisories with no patched release at any version (image-size×2,adm-zip,paste,derivative) and the crates pinned by the Soroban SDK the contracts build against (soroban-env-host,stellar-xdr) — seeMAINNET_READINESS.md§7. -
Explicit
TRUST_PROXY: the Expresstrust proxysetting is now disabled by default (forwarding headers ignored) instead of defaulting to1. A directly-exposed gateway can no longer be tricked into honouring a forgedX-Forwarded-For; production starts log a warning when it is unset. SetTRUST_PROXY=1/loopback/a proxy IP list only behind a real proxy. -
Wallet-based rate limiting: the paid tier is now keyed by the server-verified payer wallet on the confirmed payment row, not the client IP, so rotating source addresses cannot mint fresh buckets. Unpaid requests remain per-IP.
-
Payout concurrency: proposal creation is serialised per provider across gateway instances and across the admin API, so a multi-replica deployment (or a retried request) cannot commission two payouts for the same revenue. See the
Fixedentry below for the failure this closes. -
Payout hardening: payout automation validates
payoutWalletAddresswithStrKey.isValidEd25519PublicKey, re-checks provider approval/active state at proposal time, and refuses to pay when the destination changed. The threshold-1 auto-approve now derives the signer address from the signing key and records the real approver.
- Payout proposals can no longer be double-proposed.
pendingRevenueis a read-modify-write against thePayoutProposalledger whose write lands several awaited steps after the read (a Soroban round-trip). Two writers interleaving inside that window both observe the samealreadyReservedand both reserve the whole balance; with a threshold-1 wallet both proposals auto-execute, paying the provider twice for one revenue stream. There are two writers:PayoutsService's daily@Cron, which NestJS fires in every replica —infrastructure/kubernetes/gateway.yamlruns 2, and its README invites raising that — andAdminService.proposePayout, which a double-submitted or client-retried admin request hits twice. Both now take a per-provider Redis lock (x402:lock:payout-propose:<providerId>) around the read→reserve→propose sequence, so they exclude each other and unrelated providers stay independent. Fail-closed: if the lock cannot be taken (Redis unreachable) the proposal is refused — a skipped provider loses nothing because its revenue staysconfirmed, whereas a duplicate proposal moves money that no revenue backs.AdminServicereturns 409 when a proposal for the same provider is already in flight. - Credit-escrow settlement now actually settles — exactly once. Three
defects in the
#25wiring meant the documented behaviour did not hold: (1) an escrow draw was pre-charged bychargeEscrowOnChainbeforesettleEscrowran, so the contract's per-quote idempotency guard made the settlement charge fail and the surplus was never refunded; (2) a flat-rate route settled through the escrow payment path was never debited at all, giving a funded caller unlimited free requests; (3) every metered Horizon payment was settled against the caller's escrow balance too, double-billing wallets that held one. Escrow settlement is now a single call site, applies to escrow-funded draws (X-Escrow-User) for both flat-rate and per-token routes, and never touches escrow for a Horizon-paid request. The e2e suite now asserts the balance is consumed (previously it passed even when nothing was charged). - Payout automation no longer re-proposes committed revenue. Pending
payout revenue was computed as
confirmed − executed, so an M-of-N proposal awaiting signer approvals (threshold > 1) did not reserve its revenue and the daily run minted a new proposal for the same money every day — two approvals could have paid a provider twice. In-flight proposals (pending/proposed/approved) now reserve revenue in both the cron and the admin endpoint (PAYOUT_RESERVING_STATUSES). - Prisma migration drift reconciled.
schema.prismadeclared@@unique([txHash]), but the migration created only a partial unique index, soprisma migrate diffreported permanent drift andprisma db pushproduced a different database thanprisma migrate deploy. The canonical full unique index now matches the declared schema; the single-use guarantee is unchanged (verified against a real Postgres). amountToScValnow encodes the full i128 range. It packed the whole value into the low word and hardcodedhi = 0, so any amount ≥ 2^64 threw or encoded incorrectly. The value is now split into low/high 64-bit words with an expliciti128upper bound.X-Payment-Receiptnow actually carries the route. #46 populated the route in the persistedreceiptJson, but the returned header (and the streamingx402_receiptevent) omitted the field entirely — the acceptance criterion required both. All receipt payloads now includeroute, with e2e assertions.- Paid retry now succeeds end to end.
POST /api/v1/chat/completionswith a validX-Payment-Hashpreviously returned402 "Payment was made before the quote was issued". At retry time noPaymentrow carried the hash yet (the quote's row was stillpendingwithtxHash = NULL), so verification minted a new quote and validated the payment timestamp against it — always rejecting a payment made before that new quote. The quote memo is a deterministic function of the quote id, so the gateway now resolves the originating quote from the transaction's on-chain memo (PaymentsService.findPendingByQuoteMemo+X402Service.fetchTransactionMemo) and binds the payment to the exact quote window it paid for. Payments with no resolvable quote are still rejected by the fresh quote'sissuedAtlower bound — fail-closed. Covered by new unit tests (quoteMemo/quoteIdPrefixFromMemo,findPendingByQuoteMemo) and two e2e cases;scripts/testnet-journey.shnow passes itsHTTP 200step. - Live payout-leg deploy updated for the constructor ABI.
scripts/testnet-payout.tsdeployed a fresh multisig with raw SDK operations and then called theinitentry point — which the atomic-initialization change removed, so the payout leg of the live journey would have failed at deploy. It now deploys through thestellarCLI with the constructor arguments (-- --signers … --threshold 1 --token …), the same mechanism asscripts/deploy-contracts.sh; the bundled@stellar/stellar-sdk12.x predates the protocol-23CREATE_CONTRACT_V2host function, so it cannot carry constructor arguments from TypeScript.scripts/testnet-journey.shnow lists the CLI as a requirement of that leg. - Video forged-hash demo: the capture used a hardcoded
f×64 hash, which replay protection claimed on first sight, so later captures reported "Payment already used" instead of the intended fail-closed "Transaction not found on chain". Captures now use a random unseen hash. - SSE payment receipts are now reliable: the upstream
data: [DONE]sentinel is withheld and re-emitted after the trailingx402_receiptevent, so clients that stop at[DONE]still receive the receipt; the SDK drains past[DONE]and exposes the final receipt via lazy getters. - SQL time-series analytics: window starts are aligned to the interval grid, so aggregated rows are no longer silently dropped for the (usual) unaligned wall-clock time.
- Escrow draws: each draw now carries a unique
escrow:<quoteId>synthetic hash, so a second escrow request no longer collides with the uniquePayment.txHashindex and per-token settlement actually runs. - Circuit breaker: an unexpected (non-
open:<n>) Redis reply no longer fast-fails every request against a healthy upstream. - Dashboard
cn(): now actually merges classes viatwMerge(clsx(...))instead of a naivejoin(' '), so conflicting Tailwind utilities resolve as the call sites (and tests) intend. - Dashboard test target: wired
nx test dashboard(and added thejest-environment-jsdomthe config already required) — the existing spec files were previously never executed in CI.
schema-driftCI job: applies the Prisma migration history to an empty Postgres and runsprisma migrate diff --exit-code, so a declared-but-not- materialised constraint can never silently diverge from the migrations again. Also removed theWallet/PrepaidCreditseeds fromscripts/backup-restore-drill.shandvideo/seed-demo.sql(the models are gone); the drill was re-run end to end and passes 14/14.- Product pitch video (
docs/media/x402-gateway-demo.mp4, 1080p, ~5 min) with thumbnail, burned-in captions, an.srtand a synthesized voice-over, featured in the README. It is rendered from a deterministic stage fed by assets captured from a live gateway + dashboard, and now shows the full paid flow: a real Stellar testnet USDC payment, a200with payment receipt, then replay and forged-hash rejection. Pipeline and provenance:video/README.md. - Provider-agnostic narration:
video/make-voiceover.mjssynthesizes the voice-over with ElevenLabs, OpenAI, Cartesia or Gemini (all normalized to 24 kHz mono), or entirely locally with piper when no API key is available, and muxes it onto the video. Each cue is synthesized and placed at its own caption time, so a caption changes exactly when its line starts being spoken. pnpm video:check(and aVideo Narration TimingCI job) fails the build when any narration cue would overrun the scene budget it is spoken over.- Persisted in-app notifications:
POST/GET /api/v1/notificationsbacked by a PostgresNotificationrow (withread/readAtstate), plus a dashboard/notificationsfeed with read controls. Migration20260912000000_notification_read_state.
- New coverage for wallet-keyed rate limiting, payout validation/approval,
escrow hash uniqueness, notification persistence, analytics bucket
alignment, SSE receipt ordering, and the
minPaymentAmountfloor.
- Quote-window integrity:
issuedAtadded toQuote; payments made before quote issuance are rejected (prevents replay of pre-issuance txs) - Network timeouts: config-driven
HORIZON_TIMEOUT_MS/SOROBAN_RPC_TIMEOUT_MSwired through x402-core and contract clients - Redis fail-fast: bounded retry strategy + connect timeout so a down Redis fails startup instead of hanging forever
- Dependency posture: NestJS 10 → 11.2.3 (Express 5.2.1, multer
2.2.0), Next 14 → 15.5.25 (React 19), nx 19.5 → 22.7.9
(eslint-config-prettier 10), plus overrides for express/ws/body-parser/qs/
uuid/lodash/js-yaml/toml/postcss/file-type/minimatch/serialize-javascript/
fast-uri/adm-zip — 0 critical, 0 runtime-reachable advisories; the 9
dev-tooling advisories dropped to 2 high with the nx 22 migration, both
image-size(via the unused @nx/vite→less chain; no patched release exists) — seeMAINNET_READINESS.md§7 - Build fix: the nx 22 tree pulled
supports-color@7.2.0into the@babel/corepeer chain, splittingnextinto two store instances (root.bin/nextvsapps/dashboardresolved different copies, breaking the pages-router/404prerender with the<Html>context error); pinningsupports-color: 8.1.1collapses the tree to one instance —next buildgreen again - Input validation: message/content bounds hardened in
@x402/validation
- Prometheus
/metricsendpoint with HTTP request counters/durations, provider, debt, and circuit-breaker metrics + Grafana dashboard - Liveness/readiness:
/health/liveand/health/readywith real Postgres/Redis dependency checks (503 when unhealthy) - Streaming backpressure in proxy stream forwarding
- Docker hardening: non-root users, healthchecks, OCI labels
- CI/CD: gitleaks, trivy, osv-scanner, SBOM (CycloneDX) jobs; pnpm 11
migration (workspace
allowBuilds/overrides)
- New:
ARCHITECTURE.md,THREAT-MODEL.md,API.md,GAS-OPTIMIZATION.md,OPERATIONS.md(RTO/RPO, backup/DR),OBSERVABILITY.md; rewrittenAUDIT.md; updatedSECURITY.md/DEPLOYMENT.md/MAINNET_READINESS.md/README.md
- Gateway: Reverse proxy with HTTP 402 Payment Required flow for LLM APIs
- Gateway: Flat-rate and per-token pricing models with metered billing
- Gateway: Triple-layered replay protection (Redis SET NX → on-chain contract → DB unique constraint)
- Gateway: SSRF guards for webhook and upstream URLs with DNS resolution
- Gateway: Rate limiting (paid/unpaid tiers, sliding-window Redis Lua script)
- Gateway: Circuit breaker for upstream LLM failures
- Gateway: Streaming (SSE) support for chat completions
- Gateway: Multi-tenant isolation — all data scoped by authenticated wallet
- Gateway: Audit logging of all gateway operations
- Gateway: Webhook notifications with HMAC signatures and retry logic
- Gateway: Wallet-based authentication (challenge-response with Stellar keys)
- Gateway: Escrow settlement via credit-escrow Soroban contract (charge + refund)
- Dashboard: Next.js provider dashboard with route/payment management
- Dashboard: Real-time analytics (summary, time series, top callers/routes)
- Dashboard: Wallet authentication (Freighter, xBull, Albedo)
- SDK: TypeScript client with automatic 402 → pay → retry flow
- SDK: Streaming support via async generators
- SDK: External wallet signing (publicKey + signTransaction)
- Contracts: Payment Verifier — on-chain payment recording with replay protection
- Contracts: Credit Escrow — prepaid balance management with idempotent charge/refund
- Contracts: Multisig Wallet — M-of-N signer approval for provider payouts
- CI/CD: Lint → unit tests (coverage thresholds) → E2E → contract tests → security audit
- CI/CD: Docker images for gateway and dashboard
- CI/CD: Railway + Vercel deployment configs
- Docs: README, DEPLOYMENT.md, SECURITY.md, CONTRIBUTING.md, AUDIT.md
- C2: SDK external signer path now works (
publicKey+signTransaction) - C5: Streaming responses now include receipt/cost as trailing SSE event
- M2:
minPaymentAmountenforced in quote generation and payment verification - M4: RateLimitGuard added to PaymentsController public status endpoint
- C1: Escrow settlement wired into proxy controller (charge + auto-refund surplus)
- M6: DNS rebinding protection added at proxy-forward time (
813fed7) - M10: Email notification channel wired with nodemailer (
09e4706) - L9: Jest unit test scaffolding added for dashboard pages and components (
05e10c9) - M3: Credit-escrow invariant tests added for balance equation (
b49a2d1) - L4: CHANGELOG.md, git tags, and release cadence established
- L2:
contracts/deployed-addresses.jsoncommitted and tracked
Kept for historical accuracy, not as a current status. Four of these were fixed in the [Unreleased] section above and are struck through here so the two sections cannot be read as contradicting each other.
- Circuit breaker is in-memory only (not shared across gateway instances) —
still true today; tracked in
MAINNET_READINESS.md. SDK unit tests remain at 0% coverage (#45)— resolved;packages/sdknow has a Jest target with a suite covering thecall/callStream/signer paths.Escrow settlement is partially wired (credit-escrow contract exists but gateway settlement path is incomplete — #25)— resolved; a single settlement call site charges the actual cost and refunds the surplus (see [Unreleased] → Fixed).Streaming receipt headers are not yet set (— resolved; the gateway emits a trailingX-Payment-Receiptempty on SSE — #29)x402_receiptSSE event and the SDK reads past[DONE]to surface it.API key / session tables in Prisma schema are dead code — #47— resolved; both models are dropped (20260812000000_remove_session_apikey_models), and the later deadWallet/PrepaidCreditmodels were dropped too (20260913000000_remove_unused_wallet_prepaidcredit).