Seller guide: charge for your own API
You are the seller: you host an API and want buyers to pay per call. This is the full integration. It is four steps: advertise a price, verify the payment, settle it, and wait for confirmation before you serve.
Examples are TypeScript with fetch, and work unchanged in JavaScript if you drop the type annotations. Nothing here needs a chain library on the server: the facilitator does the on-chain work.
1. Get a key and set your environment
Mint a seller API key on the facilitator dashboard. It is shown once. See Seller API keys for how to store and rotate it.
# .env on your server. Never in a browser bundle, never sent to buyers.
FACILITATOR_URL=https://public-facilitator.xdcai.tech
FACILITATOR_API_KEY=xdcai_live_...
SELLER_RECEIVER_ADDRESS=0xYourReceivingAddress
TOKEN_ASSET=0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1
TOKEN_NAME=USDC
TOKEN_VERSION=2
Prices are integer strings in the token's smallest unit. USDC has 6 decimals, so "10000" is 0.01 USDC. Do not send a float.
2. Answer 402 with your price
When a request arrives with no payment header, tell the caller what to pay.
import express from "express";
const app = express();
const requirements = {
scheme: "exact",
network: "eip155:50",
amount: "10000", // 0.01 USDC
payTo: process.env.SELLER_RECEIVER_ADDRESS!,
asset: process.env.TOKEN_ASSET!,
maxTimeoutSeconds: 300,
extra: { name: "USDC", version: "2", assetTransferMethod: "eip3009" },
};
app.get("/api/data", async (req, res) => {
const header = req.header("PAYMENT-SIGNATURE") ?? req.header("X-PAYMENT");
if (!header) {
return res.status(402).json({
x402Version: 2,
accepts: [requirements],
resource: { url: "https://your.api/api/data", mimeType: "application/json" },
});
}
// continues in step 3
});
PAYMENT-SIGNATURE is the x402 v2 header. X-PAYMENT is v1. Accept both so older clients keep working.
3. Verify, then settle
Both endpoints take the same body and the same Authorization header.
const auth = { Authorization: `Bearer ${process.env.FACILITATOR_API_KEY}` };
const json = { ...auth, "content-type": "application/json" };
const paymentPayload = JSON.parse(Buffer.from(header, "base64").toString());
const body = { x402Version: 2, paymentPayload, paymentRequirements: requirements };
// Verify is free and never touches your credits.
const verify = await fetch(`${process.env.FACILITATOR_URL}/verify`, {
method: "POST",
headers: json,
body: JSON.stringify(body),
}).then((r) => r.json());
if (!verify.isValid) {
return res.status(402).json({ error: "buyer_payment_invalid" });
}
const settle = await fetch(`${process.env.FACILITATOR_URL}/settle`, {
method: "POST",
headers: {
...json,
// The EIP-3009 nonce is unique per authorization and stable across retries,
// so a retry can never broadcast the same payment twice.
"Idempotency-Key": paymentPayload.payload.authorization.nonce,
},
body: JSON.stringify(body),
}).then((r) => r.json());
Idempotency-Key is required on /settle.
4. Wait for confirmation, then serve
:::danger Settlement is asynchronous
/settle can return 202 settlement_queued (accepted, nothing broadcast yet) or settlement_pending (broadcast, not yet mined). Neither is a payment. Serve paid content only when the response has success: true and a transaction hash.
:::
let result = settle;
for (let i = 0; i < 10 && !result.success && result.settlementId; i++) {
await new Promise((r) => setTimeout(r, 1500));
result = await fetch(
`${process.env.FACILITATOR_URL}/settlements/${result.settlementId}`,
{ headers: auth },
).then((r) => r.json());
}
if (!result.success || !result.transaction) {
// Not necessarily a failure: it may still confirm. Reconcile by settlementId
// later rather than settling the same payment again.
return res.status(402).json({ error: "facilitator_settlement_issue" });
}
res.setHeader("PAYMENT-RESPONSE", Buffer.from(JSON.stringify(result)).toString("base64"));
res.json({ data: "your paid content" });
A settlement whose status is failed, expired, canceled or rejected is terminal. Anything else with a settlementId is still in flight.
Telling the two kinds of failure apart
A buyer whose payment was fine should never be told their payment failed.
| Response | What it means | What to tell the buyer |
|---|---|---|
401 invalid_key, revoked_key | Your key is wrong or revoked | Nothing. Fix your server |
403 insufficient_facilitator_credits | Your prepaid credits ran out | Nothing. Top up |
403 seller_plan_inactive | Your plan is suspended | Nothing. Check billing |
429 rate_limited | Your key's rate limit | Retry with backoff |
verify.isValid: false | The payment really is invalid | Ask them to pay again |
202 settlement_queued | Accepted, not yet paid | Keep waiting. Do not serve |
A TypeScript SDK is coming
The facilitator team is preparing @xdcai/x402-seller, which wraps all of the above in a single express or hono middleware using the same environment variables. It is not published to npm yet, so the calls on this page are the working path today.
Or skip the server work
If you would rather not implement any of this, the gateway wraps your existing API and does the 402, the verification and the settlement for you. No key, no credits, no polling loop, and your service is listed in the marketplace.