Skip to main content

Seller guide: charge for your own API

You are the seller: you host an API and want buyers to pay per call. There are two ways to do it, a ready-made middleware or the full hand-rolled flow. Both do the same four things: advertise a price, verify the payment, settle it, and wait for confirmation before serving. If you would rather write no payment code at all, the gateway does all of it for you and lists your service in the marketplace.

Seller API keys​

The middleware and the raw HTTP paths both need a seller API key, which authenticates your server to the facilitator. It is not a payment credential, and buyers never see one. (The gateway needs no key.)

What the key looks like​

xdcai_live_<keyId>_<secret>

There is no separate access key and secret key. It is one bearer string, with the public keyId embedded in it. The facilitator stores only sha256(keyId || secret), so the secret cannot be recovered by anyone, including us.

PartPublicUsed for
keyIdYesIdentifying the key in the dashboard, in logs, and when revoking
secretNoProving the request is yours

Create one​

  1. Open the facilitator dashboard.
  2. Choose the address your API is paid at.
  3. Select New key, label it after the deployment that will use it, and create it.
  4. Copy the key from the dialog. It is shown once and there is no way to see it again.

Creating a key is not idempotent: pressing the button twice mints two keys. Any signed-in account can create keys for an address it owns, and only for an address it owns.

Set your environment​

# .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.

Keep it server-side​

warning

Anyone holding your key can ask the facilitator to settle payments on your account and can spend your credits. It grants no access to your funds and cannot move USDC out of your wallet, but treat it like a password.

  • Never ship it to a browser, a mobile app, or any client-side bundle.
  • Never give it to buyers. They do not need one.
  • Use one key per deployment, so revoking staging does not break production.
  • Keep it out of logs. Log the keyId if you need to correlate, never the whole string.

Rate limits​

Limits are per key, not per buyer.

PlanRequests per minuteBurst
free6020
payg600100
enterprise3000500

Exceeding them returns 429 rate_limited. The limit that applies is the one on the key, not the verifyRpm and settleRpm shown on your plan.

Rotate and revoke​

Rotate without downtime:

  1. Create the new key.
  2. Deploy it to the server.
  3. Confirm traffic is flowing.
  4. Revoke the old key.

Revoking takes effect immediately, and a revoked key returns 401 revoked_key. Revoking a key that is already revoked is safe and does nothing.

Pick your path​

@xdcai/x402-seller wraps the whole flow, the 402 response, verification, settlement and the confirmation poll, into one Express or Hono middleware. Published by the facilitator team (Apache-2.0, Node 20+). Buyers still stay accountless.

npm install @xdcai/x402-seller@1.0.0-rc.1

:::warning Pin the exact version This is a release candidate. Both latest and next point at 1.0.0-rc.1 today, so pin the exact version and read the release notes before a major upgrade. :::

:::note You do not write wallet-specific code You return the challenge, the buyer's SDK builds the payment, and the facilitator verifies it. Whether the buyer is an ordinary address or a deployed contract wallet is the facilitator's problem, not yours, and it needed no seller release. Paid content is released only after a confirmed settlement either way. :::

Express:

import express from "express";
import { paymentMiddleware, XDC_USDC } from "@xdcai/x402-seller";

const app = express();

app.use(paymentMiddleware({
facilitatorUrl: process.env.FACILITATOR_URL!,
facilitatorApiKey: process.env.FACILITATOR_API_KEY!, // server side only
seller: { receiver: process.env.SELLER_RECEIVER_ADDRESS! },
routes: {
"GET /api/data": {
price: "10000", // atomic USDC, so 0.01 USDC
asset: process.env.TOKEN_ASSET ?? XDC_USDC.asset,
network: XDC_USDC.network,
description: "Premium data endpoint",
tokenName: process.env.TOKEN_NAME ?? XDC_USDC.eip712.name,
tokenVersion: process.env.TOKEN_VERSION ?? XDC_USDC.eip712.version,
},
},
}));

app.get("/api/data", (_req, res) => {
res.json({ ok: true, data: "paid content" });
});

The route handler only runs after a payment settles on-chain; an unpaid request never reaches it.

Hono on Node uses the same config object:

import { Hono } from "hono";
import { honoPaymentMiddleware, XDC_USDC } from "@xdcai/x402-seller";

const app = new Hono();
app.use("*", honoPaymentMiddleware({ /* same config as Express */ }));
app.get("/api/data", (c) => c.json({ ok: true }));

Other exports: createSellerGuard (framework-agnostic), XDC_USDC, SellerSdkError, and the header constants. Subpath imports @xdcai/x402-seller/express and /hono are available too.

:::warning Name must be "USDC", not "USD Coin" Using USD Coin for this contract produces a different EIP-712 domain separator, and the signed authorization reverts on settlement. GET /supported does not return the token address or the EIP-712 name and version, which is why the SDK ships them as the XDC_USDC constant. Use it rather than hardcoding. :::

The SDK sorts failures into categories so you can tell a buyer problem from a seller problem, and buyer-visible bodies never contain your key. Subscribe to onEvent to log each one:

CategoryHTTPWhat to do
payment_required402Normal challenge, buyer pays and retries
buyer_payment_invalid402Bad proof, do not blind-retry
settlement_failed402Investigate, do not duplicate the payment
seller_api_key_invalid503Rotate or fix your key
seller_rate_limited503Back off
seller_out_of_facilitator_credits503Top up your credits
facilitator_unavailable503Retry with backoff
misconfigured_sdk500Fix your config

It is exact scheme only (scheme=exact, network eip155:50); batch and channel settlement are not in this version.

Telling the two kinds of failure apart​

Whichever path you use, a buyer whose payment was fine should never be told their payment failed.

ResponseWhat it meansWhat to tell the buyer
401 invalid_key, revoked_keyYour key is wrong or revokedNothing. Fix your server
403 insufficient_facilitator_creditsYour prepaid credits ran outNothing. Top up
403 seller_plan_inactiveYour plan is suspendedNothing. Check billing
429 rate_limitedYour key's rate limitRetry with backoff
verify.isValid: falseThe payment really is invalidAsk them to pay again
202 settlement_queuedAccepted, not yet paidKeep waiting. Do not serve