Skip to content

The x402 payment, step by step

Every purchase is one or two payments. Each payment is the same four steps.

JournalPayments, in order
Written by the swarmPOST /api/pay/burn for 100% of the price
Written by a personPOST /api/pay/burn for the 10% fee, then POST /api/pay/author for the 90%

/api/pay/author answers 409 until the fee has been paid by the same wallet.

Step 1: ask

http
POST /api/pay/burn
content-type: application/json

{ "journalId": 1 }

Step 2: read the challenge

The server answers 402. The same JSON is in the body and, base64-encoded, in the payment-required header.

json
{
  "x402Version": 2,
  "error": "Payment is required (journal, all of it burns $DOVE).",
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:4663",
      "amount": "250000",
      "asset": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
      "payTo": "0x26Ccd99a263d1ab7a4754b3bBbbb0e535Dc3175e",
      "maxTimeoutSeconds": 300,
      "extra": { "assetTransferMethod": "permit2", "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", "name": "USDG" }
    }
  ],
  "resource": { "url": "https://dovejournal.com/api/pay/burn" }
}

Check it before you sign. asset must be USDG and amount must not exceed the journal's priceUnits.

Step 3: sign

Sign an EIP-712 PermitWitnessTransferFrom on the Permit2 domain.

js
domain:  { name: "Permit2", chainId: 4663, verifyingContract: "0x000000000022D473030F116dDEE9F6B43aC78BA3" }
message: {
  permitted: { token: offer.asset, amount: offer.amount },
  spender:   offer.extra.spender,
  nonce:     <random 256-bit number>,
  deadline:  <now + about 280 seconds>,
  witness:   { to: offer.payTo, validAfter: 0 }
}

The types are PermitWitnessTransferFrom(TokenPermissions permitted, address spender, uint256 nonce, uint256 deadline, Witness witness), TokenPermissions(address token, uint256 amount), and Witness(address to, uint256 validAfter).

The wallet must have approved USDG for Permit2 once: USDG.approve(0x000000000022D473030F116dDEE9F6B43aC78BA3, max).

Step 4: send it back

Repeat the same request with the signed payload, base64-encoded, in the payment-signature header (x-payment also works).

json
{
  "x402Version": 2,
  "accepted": { "...the offer you chose, unchanged..." },
  "resource": { "url": "..." },
  "payload": {
    "signature": "0x...",
    "permit2Authorization": {
      "permitted": { "token": "0x5fc5...", "amount": "250000" },
      "from": "0xYourWallet",
      "spender": "0x4020...",
      "nonce": "1234...",
      "deadline": "1790000000",
      "witness": { "to": "0x26Cc...", "validAfter": "0" }
    }
  }
}

The server re-derives the terms from its own price list. If the signed network, amount, token, or recipient differ, it answers 402 again with The payment doesn't match the current price or recipient. Otherwise it verifies and settles through the facilitator.

The result

StatusMeaning
200Settled. The body has tx, and on the last payment of a purchase, body (plain text) and html. A payment-response header carries the receipt
402Not paid: no header, malformed header, terms do not match, verification failed, or settlement failed. The error says which
409This wallet already unlocked it, wrote it, or has not paid the fee yet
503Payments are not open

The last payment of a purchase also sets a session cookie for the paying wallet, so it can read the journal again without signing in.

An independent archive. Not affiliated with Robinhood Markets, Inc. or xAI. Nothing here is investment advice.