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

::: info 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);
```

::: tip 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](/x402/reference#reason-codes).

## Getting testnet funds

Base Sepolia ETH from a [Base faucet](https://docs.base.org/chain/network-faucets),
then swap or use Circle's testnet USDC faucet. You only need USDC to *pay*; the
facilitator covers gas.

## Common mistakes

| Symptom | Cause |
| --- | --- |
| `invalid_exact_evm_recipient_mismatch` | The signed `to` isn't the `payTo` you sent in `paymentRequirements`. They must match exactly. |
| `invalid_exact_evm_authorization_expired` | `validBefore` already passed — often clock skew. Sign `validAfter` a minute in the past. |
| `invalid_exact_evm_signature` | Wrong EIP-712 domain. `name`/`version` must come from `extra`, `chainId` must be `84532`, `verifyingContract` the token. |
| `x402_amount_below_minimum` | Under 0.01 USDC. Gas costs more than the payment below that floor. |
| `x402_asset_not_allowed` | Only testnet USDC is settled today. |
