Docs/Merchants

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

text
[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:

  1. Decodes the PAYMENT-SIGNATURE base64 envelope into the fields {scheme, queueSignature, callbackSignature?, recipientEtaAddress, asset, amount, resource}.
  2. Sanity-checks the envelope:
    • scheme is "umbra-mixer-v1".
    • recipientEtaAddress matches your configured merchantEtaAddress. (Stops an agent from re-presenting an envelope intended for a different merchant.)
    • asset, amount, resource all match this route's expectations.
    • queueSignature hasn't been seen in the past 5 minutes.
  3. Reads on-chain — calls getTransaction(queueSignature) and, if present, getTransaction(callbackSignature) via the configured RPC. Both must exist and have landed without error.
  4. Attaches an X-Payment-Response header (base64 envelope of the settlement) and res.locals.obscuraSettlement (parsed object).
  5. 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.