Packages
The payment mechanism
x402 is a challenge-response protocol built on the HTTP 402 status code:1
Request without payment
The agent (via
nymor-server) requests a resource with no payment attached.2
402 Payment Required
nymor-resources returns 402, with the payment requirements (price, network, destination address) encoded in a PAYMENT-REQUIRED response header — not the JSON body. This is an x402/express convention, confirmed by inspecting a real response rather than assumed from docs, and it mattered: it’s the exact detail that broke the dashboard’s browser-based payment flow until CORS was fixed to expose that header.3
Sign and pay
The buyer signs a Stellar transaction invoking
transfer on the USDC Stellar Asset Contract (SAC) — this is a Soroban invoke_host_function operation, not a classic Payment operation, which matters for anything reading payment history off-chain (see the dashboard’s live-activity panel). The signed payment is submitted to the OZ Channels facilitator for verification and settlement.4
Retry with proof
The original request is retried with an
X-PAYMENT header carrying the payment payload. The resource responds with real data, and a X-PAYMENT-RESPONSE header carrying the settled Stellar transaction hash.The spend-gate: two layers, not one
Before any payment happens,nymor-server checks a budget. There are two enforcement layers, and — this is the detail worth being precise about — they are not currently connected to each other:
- Local ledger (
nymor.ledger.json) — an atomic reserve/confirm/release pattern under a file lock (reserveSpend→confirmReservationorreleaseReservation), proven race-safe with a 20-way concurrency test. This is what actually gates every real paymentnymor-servermakes today. - On-chain policy (
nymor-account+nymor-spending-limit-policy) — a Soroban smart account that refuses to authorize a transfer past its cap, enforced by the Stellar network itself, not application code. Deployed and proven on testnet with real transactions — but not yet wired into the buyer’s live signing path. Full detail, including why, in On-chain policy.
nymor.spend_status and human-readable “why was this blocked” explanations. The on-chain layer is the enforcement that can’t be bypassed by a bug in nymor-server’s own code; the local layer is the one that’s actually live.
Buyer signing today
payment.ts signs with a raw Ed25519 keypair (createEd25519Signer from @x402/stellar), not through the smart account. This is a deliberate, disclosed gap — see On-chain policy for the specific library limitation that caused it.