Skip to content

x402 quickstart

Two sides to build: the seller (charge for an endpoint) and the buyer (pay for one). Both talk to the same facilitator, and neither needs a Kryard API key — the x402 routes are unauthenticated.

Base URL

https://api.kryard.com · network base-sepolia · asset: testnet USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e

Check what's live before you start:

bash
curl -s https://api.kryard.com/x402/v1/supported
json
{
  "kinds": [{ "x402Version": 1, "scheme": "exact", "network": "base-sepolia" }],
  "signers": { "eip155:*": ["0x433ff8253Faa9A57b6326f69Abc8Ad0bf208f5c1"] }
}

signers is the relayer address that will front gas for your settlements.


Seller — charge for an endpoint

1. Answer unpaid requests with 402

Describe your price in an accepts entry. extra carries the token's EIP-712 domain, which the buyer needs to sign.

ts
const PRICE = {
  scheme: "exact",
  network: "base-sepolia",
  maxAmountRequired: "10000",           // 0.01 USDC — USDC has 6 decimals
  asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  payTo: "0xYourMerchantAddress",
  resource: "https://your-api.example/report",
  maxTimeoutSeconds: 120,
  extra: { name: "USDC", version: "2" },
};

app.get("/report", async (c) => {
  const header = c.req.header("x-payment");
  if (!header) {
    return c.json({ x402Version: 1, accepts: [PRICE] }, 402);
  }
  // …continue to step 2
});

2. Verify, serve, then settle

ts
const FACILITATOR = "https://api.kryard.com/x402/v1";
const paymentPayload = JSON.parse(atob(header));

const verify = await fetch(`${FACILITATOR}/verify`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ paymentPayload, paymentRequirements: PRICE }),
}).then((r) => r.json());

if (!verify.isValid) {
  return c.json({ x402Version: 1, accepts: [PRICE], error: verify.invalidReason }, 402);
}

// Do the work FIRST — you don't want to charge for a response you can't deliver.
const report = await buildReport();

const settled = await fetch(`${FACILITATOR}/settle`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ paymentPayload, paymentRequirements: PRICE }),
}).then((r) => r.json());

if (!settled.success) {
  return c.json({ x402Version: 1, accepts: [PRICE], error: settled.errorReason }, 402);
}

c.header("X-PAYMENT-RESPONSE", btoa(JSON.stringify(settled)));
return c.json(report);

Settling twice is safe

The facilitator keys its ledger on (chain, asset, payer, nonce) — EIP-3009's own identity for an authorization. Replaying the same /settle returns the original transaction hash instead of broadcasting a second transfer. Retry freely on a network error.


Buyer — pay for an endpoint

You need testnet USDC on Base Sepolia. You do not need ETH.

1. Read the price

ts
const res = await fetch("https://your-api.example/report");
if (res.status !== 402) return res;
const { accepts } = await res.json();
const req = accepts[0];

2. Sign an EIP-3009 authorization

The nonce is a random 32-byte value you choose — it's scoped on-chain to (token, you), so it only has to be unique for your own address.

ts
import { createWalletClient, custom, toHex } from "viem";
import { baseSepolia } from "viem/chains";

const wallet = createWalletClient({ chain: baseSepolia, transport: custom(window.ethereum) });
const [from] = await wallet.getAddresses();

const now = Math.floor(Date.now() / 1000);
const authorization = {
  from,
  to: req.payTo,
  value: req.maxAmountRequired,
  validAfter: String(now - 60),                        // clock-skew slack
  validBefore: String(now + req.maxTimeoutSeconds),
  nonce: toHex(crypto.getRandomValues(new Uint8Array(32))),
};

const signature = await wallet.signTypedData({
  account: from,
  domain: {
    name: req.extra.name,
    version: req.extra.version,
    chainId: 84532,
    verifyingContract: req.asset,
  },
  types: {
    TransferWithAuthorization: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
      { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" },
      { name: "nonce", type: "bytes32" },
    ],
  },
  primaryType: "TransferWithAuthorization",
  message: {
    ...authorization,
    value: BigInt(authorization.value),
    validAfter: BigInt(authorization.validAfter),
    validBefore: BigInt(authorization.validBefore),
  },
});

Signing is free and off-chain. Nothing has moved yet.

3. Retry with X-PAYMENT

ts
const paymentPayload = {
  x402Version: 1,
  scheme: "exact",
  network: "base-sepolia",
  payload: { signature, authorization },
};

const paid = await fetch("https://your-api.example/report", {
  headers: { "X-PAYMENT": btoa(JSON.stringify(paymentPayload)) },
});

If the seller settled it, X-PAYMENT-RESPONSE carries the transaction hash.


Testing without a seller

Point /verify at the facilitator directly to check a payload before wiring up an endpoint. It's read-only — it never broadcasts:

bash
curl -s -X POST https://api.kryard.com/x402/v1/verify \
  -H 'content-type: application/json' \
  -d '{"paymentPayload":{…},"paymentRequirements":{…}}'
json
{ "isValid": false, "invalidReason": "invalid_exact_evm_insufficient_balance", "payer": "0x…" }

Every failure names its cause — see reason codes.

Getting testnet funds

Base Sepolia ETH from a Base faucet, then swap or use Circle's testnet USDC faucet. You only need USDC to pay; the facilitator covers gas.

Common mistakes

SymptomCause
invalid_exact_evm_recipient_mismatchThe signed to isn't the payTo you sent in paymentRequirements. They must match exactly.
invalid_exact_evm_authorization_expiredvalidBefore already passed — often clock skew. Sign validAfter a minute in the past.
invalid_exact_evm_signatureWrong EIP-712 domain. name/version must come from extra, chainId must be 84532, verifyingContract the token.
x402_amount_below_minimumUnder 0.01 USDC. Gas costs more than the payment below that floor.
x402_asset_not_allowedOnly testnet USDC is settled today.

Wallet infrastructure for gasless, sponsored transactions — a Turnkey-compatible signer and a managed EIP-7702 relay.