Skip to main content

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:
  1. Local ledger (nymor.ledger.json) — an atomic reserve/confirm/release pattern under a file lock (reserveSpendconfirmReservation or releaseReservation), proven race-safe with a 20-way concurrency test. This is what actually gates every real payment nymor-server makes today.
  2. 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.
The local ledger stays useful regardless — it’s what powers 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.