Skip to main content

Manual x402 client flow (TypeScript)

This page shows how to perform the entire x402 flow by hand in TypeScript: no starter kit, no @x402/fetch or @x402/axios wrappers. You send raw HTTP requests, decode the 402 response, build the payment payload, and resend with the payment signature. For production youโ€™ll usually use the Fetch or Axios quickstarts; this tutorial is for agents or environments that need a minimal, dependency-light implementation or want to understand the protocol step by step.

1. Request the resource

Send a normal GET (or other method) to the protected URL. No special headers yet.
const url = "http://localhost:4021/weather";
const response = await fetch(url, { method: "GET" });

2. Handle 402 and decode PAYMENT-REQUIRED

If the server requires payment, it returns 402 Payment Required and puts the payment options in the PAYMENT-REQUIRED header (base64-encoded JSON).
if (response.status !== 402) {
  // 200: already paid or free; 4xx/5xx: handle as needed
  console.log(await response.json());
}

const paymentRequiredB64 = response.headers.get("PAYMENT-REQUIRED");
if (!paymentRequiredB64) throw new Error("402 without PAYMENT-REQUIRED header");

const paymentRequiredJson = Buffer.from(paymentRequiredB64, "base64").toString("utf-8");
const paymentRequired = JSON.parse(paymentRequiredJson) as {
  x402Version: number;
  error: string;
  resource: { url: string; description: string; mimeType?: string };
  accepts: Array<{
    scheme: string;
    network: string;
    amount: string;
    asset: string;
    payTo: string;
    maxTimeoutSeconds: number;
    extra?: Record<string, unknown>;
  }>;
  extensions?: Record<string, unknown>;
};
Exact field names and shapes are in the x402 Reference. You must use x402Version: 2 and the accepts array.

3. Choose an accepted option

Pick one entry from paymentRequired.accepts by network or scheme. For EVM (steps below), take an option whose network starts with eip155:. For Solana, take one whose network starts with solana: (see Solana (exact scheme) in step 4).
// EVM path:
const accepted = paymentRequired.accepts.find((a) => a.network.startsWith("eip155:"));
if (!accepted) throw new Error("No EVM accept option");

4. Build the payment payload

For the exact scheme on EVM, the client must produce an EIP-3009-style authorization and sign it with EIP-712 (see x402 Reference). The payload is sent in the PAYMENT-SIGNATURE header as base64-encoded JSON. Example shape (simplified; real code must use correct domain, types, and signing):
import { privateKeyToAccount } from "viem/accounts";
import { signTypedData } from "viem/accounts";

// You need: payer private key, accepted requirement, token contract (asset), validAfter/validBefore, nonce
const payload = {
  x402Version: 2,
  scheme: "exact",
  network: accepted.network,
  accepted: {
    scheme: accepted.scheme,
    network: accepted.network,
    amount: accepted.amount,
    asset: accepted.asset,
    payTo: accepted.payTo,
    maxTimeoutSeconds: accepted.maxTimeoutSeconds,
    ...(accepted.extra && { extra: accepted.extra }),
  },
  payload: {
    signature: "0x...", // EIP-712 signature from signTypedData
    authorization: {
      from: payerAddress, // Replace with your wallet address
      to: accepted.payTo,
      value: accepted.amount,
      validAfter: "0",
      validBefore: String(Math.floor(Date.now() / 1000) + 300),
      nonce: "0x..." + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex"),
    },
  },
  extensions: {},
};
const paymentSignatureB64 = Buffer.from(JSON.stringify(payload)).toString("base64");

Solana (exact scheme)

On Solana, you choose a Solana accept option (e.g. network starting with solana:), then build a partially-signed transaction and send it in the payload as base64. The transaction must contain these instructions in this order:
  1. SetComputeUnitLimit โ€” max 40,000 compute units
  2. SetComputeUnitPrice โ€” max 5 microlamports per compute unit
  3. TransferChecked โ€” token transfer (amount, mint, decimals, source, destination)
  4. Optional: Lighthouse (wallets like Phantom/Solflare may add up to two; merchants do not add these)
You need a Solana SDK (e.g. @solana/web3.js or @solana/spl-token) to build and sign the transaction. Source and destination are token accounts (e.g. associated token accounts for the mint); accepted.payTo is the recipient wallet. Decimals may be in accepted.extra. The payment payload is JSON with the transaction in payload.transaction as base64:
// 3. Choose Solana option instead of EVM
const accepted = paymentRequired.accepts.find((a) => a.network.startsWith("solana:"));
if (!accepted) throw new Error("No Solana accept option");

// 4. Build Solana transaction (pseudocode โ€” use @solana/web3.js / @solana/spl-token in practice)
// - Add setComputeUnitLimit(40_000)
// - Add setComputeUnitPrice(5)   // microlamports
// - Add createTransferCheckedInstruction: amount=accepted.amount, mint=accepted.asset,
//   decimals from token metadata or accepted.extra, source=payerTokenAccount, destination=payeeTokenAccount
// - Sign with payer keypair (partial sign; facilitator may add Lighthouse instructions)
// - Serialize transaction to buffer, then base64

// const transactionBuffer = serializedSignedTx;  // from your Solana library
const transactionB64 = Buffer.from(transactionBuffer).toString("base64");

const payload = {
  x402Version: 2,
  scheme: "exact",
  network: accepted.network,
  accepted: {
    scheme: accepted.scheme,
    network: accepted.network,
    amount: accepted.amount,
    asset: accepted.asset,
    payTo: accepted.payTo,
    maxTimeoutSeconds: accepted.maxTimeoutSeconds,
    ...(accepted.extra && { extra: accepted.extra }),
  },
  payload: {
    transaction: transactionB64,
  },
  extensions: {},
};
const paymentSignatureB64 = Buffer.from(JSON.stringify(payload)).toString("base64");
Exact instruction formats, token-account derivation, and limits are in the x402 Reference (ยง6.2 Solana).

5. Resend the request with PAYMENT-SIGNATURE

Send the same request again (same URL and method), this time adding the PAYMENT-SIGNATURE header.
const retryResponse = await fetch(url, {
  method: "GET",
  headers: {
    "PAYMENT-SIGNATURE": paymentSignatureB64,
  },
});

6. Parse the response and PAYMENT-RESPONSE

On success the server returns 200 and may include PAYMENT-RESPONSE (base64-encoded settlement details). Decode the header and parse the JSON to get settlement info.
if (!retryResponse.ok) {
  throw new Error(`Request failed: ${retryResponse.status}`);
}

const body = await retryResponse.json();
const paymentResponseB64 = retryResponse.headers.get("PAYMENT-RESPONSE");
if (paymentResponseB64) {
  const paymentResponse = JSON.parse(
    Buffer.from(paymentResponseB64, "base64").toString("utf-8")
  ) as { success: boolean; transaction?: string; network?: string; payer?: string };
  console.log("Settlement:", paymentResponse);
}
console.log(body);
Decoded PAYMENT-RESPONSE examples On success (EVM):
{
  "success": true,
  "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
  "network": "eip155:84532",
  "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66"
}
On success (Solana / SVM):
{
  "success": true,
  "transaction": "AXGcLa7sqSjt7pXV4mpVRP5a77tVjokZbgx8gkQ16X8Wgg3vDkWKMh9BrPTr1f2KrDuf9nSX7FrZEAQTkJ3y5UN",
  "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
  "payer": "6KPYDyuRnpuKcm1TerUmwLd2BcaihvhF4Ccrr8beruu2"
}
On failure (e.g. insufficient funds), the server may return a non-2xx status and/or a PAYMENT-RESPONSE header with an error. Example decoded payload:
{
  "x402Version": 2,
  "error": "Payment failed: insufficient funds",
  "accepts": [...]
}

Summary

StepAction
1GET (or other method) the resource URL
2If status is 402, decode PAYMENT-REQUIRED (base64 JSON)
3Choose one entry from accepts
4Build payment payload (EIP-712 sign for EVM exact scheme; or Solana tx) and base64-encode it
5Resend the same request with PAYMENT-SIGNATURE header
6On 200, use response body and optionally decode PAYMENT-RESPONSE
For exact field names, types, and facilitator usage, see the x402 Reference. For a ready-made client, use the Fetch or Axios quickstarts.

Need help?

Join our Community

Have questions or want to connect with other developers? Join our Discord server.