Docs

Agents

x402 over SEPA

x402 lets a server answer 402 Payment Required and a client pay by signing an authorization that a facilitator verifies and settles. This service plays the payer and the facilitator, with a bank mandate where a blockchain wallet would be.

The x402 v2 specification names sepa:eu as an example network but standardizes no fiat scheme, so this project defines one: sepa-mandate. The standing authorization is the mandate (Mandates); the per-payment proof is a signature over { from, to, value, asset, validBefore, nonce } plus the payout id and mandate reference, keyed by X402_PAYER_SECRET.

The facilitator sits on the payer's side: on SEPA only the mandate holder can move the money.

The handshake#

agent  → GET /report                        seller → 402 + PAYMENT-REQUIRED
agent    checks the mandate, creates the payout, signs
agent  → GET /report + PAYMENT-SIGNATURE    seller → POST /x402/verify, POST /x402/settle
                                            seller → 200 + resource + PAYMENT-RESPONSE

The three headers are base64 JSON. Below they are shown decoded. Everything on this page was captured from a running deployment with the simulated rail on.

The seller says 402#

GET /report — no payment attached#

Response 402, header PAYMENT-REQUIRED:

JSON
{
  "x402Version": 2,
  "resource": {
    "url": "http://127.0.0.1:4604/report",
    "description": "Q3 market report",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "sepa-mandate",
      "network": "sepa:eu",
      "amount": "1.00",
      "asset": "EUR",
      "payTo": "DE89370400440532013000",
      "maxTimeoutSeconds": 300,
      "extra": {
        "payeeName": "Report Vendor BV",
        "reference": "x402 Q3 market report"
      }
    }
  ]
}

The body carries the same object for humans. Nothing has moved. quote_x402_resource { url } reads this and commits nothing.

The facilitator#

GET/x402/supported

What this deployment can verify and settle. No key needed. Empty on a deployment with no rail, so that no seller serves a resource it can never be paid for.

With the simulated rail:

JSON
{
  "kinds": [
    {
      "x402Version": 2,
      "scheme": "sepa-mandate",
      "network": "sepa:eu",
      "extra": {
        "settlement": "sepa-simulation",
        "simulated": true,
        "environment": "SANDBOX",
        "amountFormat": "decimal-2dp"
      }
    }
  ],
  "extensions": [],
  "signers": {}
}

Without a rail:

JSON
{ "kinds": [], "extensions": [], "signers": {} }

POST/x402/verify

The seller sends the decoded PAYMENT-SIGNATURE payload together with the requirement it published. Nothing moves. The checks, in order: signature intact; not expired; matches the requirement (payee, amount, asset); the payout exists, is approved and not already settled; the payout's beneficiary is the seller's IBAN; the mandate still validates for it.

Request

JSON
{
  "x402Version": 2,
  "paymentPayload": {
    "x402Version": 2,
    "resource": {
      "url": "http://127.0.0.1:4604/report",
      "description": "Q3 market report"
    },
    "accepted": {
      "scheme": "sepa-mandate",
      "network": "sepa:eu",
      "amount": "1.00",
      "asset": "EUR",
      "payTo": "DE89370400440532013000",
      "maxTimeoutSeconds": 300,
      "extra": {
        "payeeName": "Report Vendor BV",
        "reference": "x402 Q3 market report"
      }
    },
    "payload": {
      "payoutId": "b1f75ef4-c754-4a58-8d18-bdbf0042e453",
      "mandateReference": "X402-001",
      "authorization": {
        "from": "NL91ABNA0417164300",
        "to": "DE89370400440532013000",
        "value": "1.00",
        "asset": "EUR",
        "validBefore": "1790191414",
        "nonce": "4f2c9a1e7b3d"
      },
      "signature": "sepa-mandate-sig-075e231d6fb6abd4736f9658e33c47741607c6b0e9722a2779607bf7f6d85069"
    }
  },
  "paymentRequirements": {
    "scheme": "sepa-mandate",
    "network": "sepa:eu",
    "amount": "1.00",
    "asset": "EUR",
    "payTo": "DE89370400440532013000",
    "maxTimeoutSeconds": 300,
    "extra": {
      "payeeName": "Report Vendor BV",
      "reference": "x402 Q3 market report"
    }
  }
}

Response 200

JSON
{
  "isValid": true,
  "payer": "NL91ABNA0417164300"
}

With the value edited to 5.00 after signing:

JSON
{
  "isValid": false,
  "invalidReason": "Authorization signature does not verify.",
  "payer": "NL91ABNA0417164300"
}

POST/x402/settle

Same request body as verify. Verifies again, then executes the payout through the rail.

Response 200

JSON
{
  "success": true,
  "transaction": "SIM-37F5A5A9",
  "network": "sepa:eu",
  "payer": "NL91ABNA0417164300",
  "settlement": "sepa-simulation",
  "simulated": true
}

Settle is idempotent for the same signed payload — asking again returns the same transaction, so a seller that got no answer can retry and serve. While a settlement is still pending it answers success: false with errorReason starting Settlement pending:; settle again later, do not re-pay. On a deployment with no rail it answers success: false and says it does not settle.

A seller written in Python calls the facilitator with client.x402: supported(), verify(payment_payload=..., payment_requirements=...) and settle(...). They never raise on is_valid: false or success: false; read the fields, and when result.pending is True, settle again later.

The seller serves#

GET /report — with PAYMENT-SIGNATURE#

The seller called verify, then settle, then served.

Response 200, header PAYMENT-RESPONSE:

JSON
{
  "success": true,
  "transaction": "SIM-37F5A5A9",
  "network": "sepa:eu",
  "payer": "NL91ABNA0417164300",
  "settlement": "sepa-simulation",
  "simulated": true
}

Body:

JSON
{
  "report": "Q3 market report",
  "generatedAt": "2026-09-23T19:18:34.381Z",
  "paidBy": "NL91ABNA0417164300",
  "settlement": {
    "network": "sepa:eu",
    "transaction": "SIM-37F5A5A9",
    "simulated": true
  },
  "summary": "This is the paid resource. It was served after the facilitator settled the payment in simulation; no real money moved."
}

The seller saw: 402 → payment-signature → served.

The reference seller settles before serving. The spec's order is verify → serve → settle, which works when a chain has already locked the funds; a bank transfer is not pre-funded, so serving first would extend credit. With no rail it can serve on verification alone (serve: 'after-verification') and says so in the body.

The payer side#

  • quote_x402_resource { url } — read the 402; commits nothing.
  • pay_x402_resource { url, mandateId } — checks the mandate for the quoted amount and payee, creates the payout, waits for the KYC decision, signs, retries the request. Refused by the mandate → nothing is created. Offered only on deployments that can settle.

The signature is the commitment; everything before it is reversible.

From Python, the same two steps are client.x402.quote and client.x402.pay:

Python
quote = await client.x402.quote(url="https://vendor.example/report")
if not quote.free:
    paid = await client.x402.pay(url=quote.url, mandate_id=mandate.mandate_id, reason="Q3 report")
    print(paid.paid, paid.status, paid.payout_id)

Try it#

Bash
npm run x402:walkthrough                              # no rail: the seller serves on verification
SETTLEMENT_RAIL=simulation npm run x402:walkthrough   # simulated rail: settles, then serves

Each run ends with the mandate revoked and the same request refused before anything is signed.