How it works
Every paid request goes through a two-phase handshake. The middleware handles both phases for you — this page is for debugging and integration confidence.
The swimlane
[agent SDK] [your server] [Helius RPC]
│ │ │
│ GET /article/42 │ │
│────────────────────────▶│ │
│ │ │
│ 402 + PAYMENT-REQUIRED │ │
│ (price quote, payTo, │ │
│ asset, resource) │ │
│◀────────────────────────│ │
│ │ │
│ (agent SDK posts to Obscura /api/x402/sign, │
│ which runs the Umbra mixer transfer and returns │
│ an umbra-mixer-v1 envelope) │
│ │ │
│ GET /article/42 │ │
│ PAYMENT-SIGNATURE: … │ │
│────────────────────────▶│ │
│ │ envelope sanity checks │
│ │ (recipient, asset, │
│ │ amount, resource match) │
│ │ │
│ │ getTransaction(queueSig) │
│ │──────────────────────────▶│
│ │◀──────────────────────────│
│ │ getTransaction(callback) │
│ │──────────────────────────▶│
│ │◀──────────────────────────│
│ 200 + data │ │
│ X-Payment-Response: … │ │
│◀────────────────────────│ │Phase 1 — no payment header present
The middleware computes a PaymentRequirements object from your
pay.charge() config + the route's resolved URL, then responds with:
402 Payment Required- Body: JSON
{x402Version, accepts: [requirements]} - Header:
PAYMENT-REQUIRED(same content, base64-encoded, for convenience)
The requirements.payTo field is your merchantEtaAddress — the agent
SDK forwards this challenge verbatim to Obscura, which constructs a
mixer UTXO addressed to that ETA.
Phase 2 — payment header present
On the retry, the middleware:
- Decodes the
PAYMENT-SIGNATUREbase64 envelope into the fields{scheme, queueSignature, callbackSignature?, recipientEtaAddress, asset, amount, resource}. - Sanity-checks the envelope:
schemeis"umbra-mixer-v1".recipientEtaAddressmatches your configuredmerchantEtaAddress. (Stops an agent from re-presenting an envelope intended for a different merchant.)asset,amount,resourceall match this route's expectations.queueSignaturehasn't been seen in the past 5 minutes.
- Reads on-chain — calls
getTransaction(queueSignature)and, if present,getTransaction(callbackSignature)via the configured RPC. Both must exist and have landed without error. - Attaches an
X-Payment-Responseheader (base64 envelope of the settlement) andres.locals.obscuraSettlement(parsed object). - Calls
next()— your route handler runs with the verification already complete.
What this verification does NOT do
Two things the middleware intentionally skips, because the Umbra program enforces them on-chain by construction:
- Decode the queue tx instructions to check that the mixer commitment's encrypted amount matches the envelope's claimed amount. The Umbra program rejects malformed deposits at the program level.
- Verify the Groth16 proof signature. The on-chain Umbra program
has already verified the proof when the queue tx landed; reading
getTransaction()and confirming success is sufficient.
For the hackathon scope these tradeoffs are explicit. Hardening a post-hackathon production path means adding instruction parsing for defense-in-depth.
Settlement is implicit
Unlike a classic x402-solana flow, there's no separate "settle" step
the merchant initiates — by the time the agent presents the envelope,
the queue tx has landed, Arcium MPC has finalized, and a UTXO addressed
to your ETA exists in the mixer tree. Obscura's claim daemon
(/api/cron/claim-daemon, every 2 minutes) picks it up and credits
your encrypted balance asynchronously.
Latency budget
Verification adds ~150–400ms per paid request — two getTransaction
RPC calls plus parsing. Use a low-latency RPC (Helius, Triton) if
you're optimising for sub-500ms response times.
The mixer transfer itself happens inside /api/x402/sign on Obscura's
backend, before the agent ever retries against your server. From the
merchant's perspective, the heavy 10–25-second proof + MPC round-trip
is invisible — you only see the second HTTP hit, with the envelope
already in hand.
Next: Settlement receipts — reading the settlement object.