Skip to content
07 / 12

Private x402

HTTP 402 payments from a private balance. The server gets paid and can check it; the chain shows a pool transaction, not who paid whom or how much.

Scheme
hushd
Network
near-testnet
Header
X-PAYMENT

How it works

One paid request
1Step402The resource answers with its price, the token and a hushd1… address to pay.
2StepPay privatelyThe client sends a private payment to that address through the relayer.
3StepRetryWith X-PAYMENT: the transaction, the note's commitment, and its opening sealed to the payee.
4StepCheck and serveThe server opens the note, checks the amount and that the pool made it, then serves.

The code is in packages/sdk/src/x402.ts: paymentRequired and verifyX402Payment for a server, x402Fetch for a client.

The 402 answer

A resource that wants a private payment answers 402 Payment Required with an accepts entry of scheme hushd:

{
  "x402Version": 1,
  "error": "Payment required",
  "accepts": [{
    "scheme": "hushd",
    "network": "near-testnet",
    "maxAmountRequired": "10000",
    "asset": "usdc.hushd.testnet",
    "payTo": "hushd1…",
    "resource": "https://example.com/report",
    "maxTimeoutSeconds": 120,
    "extra": { "pool": "pool.hushd.testnet" }
  }]
}

maxAmountRequired is in the token's base units (here 0.01 test USDC, which has 6 decimals), and payTo is the server's receive address.

Paying

x402Fetch fetches the URL; on a hushd 402 for this network it pays from the wallet, then fetches again with the X-PAYMENT header. You pass a cap per token, and a price above it is refused, never paid.

import { x402Fetch } from "@hushd/sdk";

const { response, paid } = await x402Fetch(wallet, "https://example.com/report", {}, {
  // the most one call may pay, per token, in base units
  maxAmount: (token) => (token === "usdc.hushd.testnet" ? 1_000_000n : 0n),
});

The header is base64 JSON with the scheme, the network and a payload of four fields: the transaction hash, the account that sent it (the relayer), the commitment of the note made for the payee, and that note's opening, sealed to the payee's key. From an agent, the MCP tool hushd_pay_x402 does all of this inside the agent's spending limits.

Checking a payment

import { paymentRequired, paymentResponse, verifyX402Payment, x402Network, TESTNET } from "@hushd/sdk";

const price = { maxAmountRequired: "10000", asset: "usdc.hushd.testnet" };

const header = req.headers.get("x-payment");
if (!header) return json(paymentRequired(TESTNET, { ...price, payTo: me.address, resource: req.url }), 402);

const paid = await verifyX402Payment(TESTNET, me, header, price, seen);
if (!paid.ok) return json({ error: paid.reason }, 402);

// serve, and say which transaction paid for it
return json(body, 200, { "x-payment-response": paymentResponse({ txHash: paid.txHash, network: x402Network(TESTNET) }) });

me is the server's Hushd identity. verifyX402Payment refuses the payment unless every one of these holds:

  1. the scheme is hushd and the network is this one,
  2. the commitment has not been used before,
  3. the opening is sealed to this payee and opens to that exact commitment under its receive key,
  4. the note is in the asked token, for at least the price,
  5. the transaction is final and successful, and
  6. the pool's transact event in that transaction created exactly that note.

Replays

seen is your record of commitments already accepted: anything with has and add. Keep it for as long as you accept payments, and on disk if the server restarts, as the reference makers do. Each payment note can then pay for one request only.

What the chain shows

One pool transact sent by the relayer, with two new note commitments, and a separate encrypted board post. Not the payer, not the payee and not the amount. The payee learns the amount and gets a note only it can spend. The payee does not learn the payer's account either: the transaction comes from the relayer.

A live example: maker quotes

The reference makers sell indicative quotes over private x402 at https://api.hushd.net/makers/<maker account>/quote?sell=&amount=&buy=, with the amount in base units. Without a payment it answers 402; with one, a quote that is valid for 30 seconds. See makers.

curl -i "https://api.hushd.net/makers/steady/quote?sell=usdc.hushd.testnet&amount=1000000000&buy=btc.hushd.testnet"
# HTTP/1.1 402 Payment Required