Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Make payments

Pay for x402 resources from an app or agent
View as Markdown

This guide pays for x402-protected resources from TypeScript with radius-sdk. createRadiusFetch returns a fetch that pays 402 Payment Required challenges in SBC and retries the request, within limits you set. To pay from a terminal or an agent's shell instead, see Pay from a terminal.

Before you start

  • A wallet funded with SBC. On testnet, see Create and fund a wallet. The wallet needs no RUSD.
  • Node.js 20 or later, or another runtime with fetch, such as Cloudflare Workers or a browser.

Install

pnpm add radius-sdk viem

Create a paying fetch

import { createRadiusFetch } from 'radius-sdk/client';
 
const payFetch = createRadiusFetch({
  network: 'testnet',
  signer: process.env.RADIUS_PRIVATE_KEY as `0x${string}`,
  maxPerRequest: '0.01 SBC',
});
 
const response = await payFetch('https://{{SELLER_HOST}}/api/lookup');
console.log(response.status, await response.text());

payFetch takes the same arguments as fetch. Responses that are not 402 come back unchanged, so you can use it for every request to a paid API.

signer accepts a private key, a viem account (for example from privateKeyToAccount), or a viem WalletClient, such as one connected to a browser wallet. Load private keys from a secret manager; never commit or log them.

Limit what you pay

maxPerRequest is required. The client refuses any offer above it before signing, with the error code price_above_limit. It also pays only on the configured network and only in SBC.

maxPerRequest limits each payment, not the total. To approve offers yourself, pass onPaymentRequired, which runs before anything is signed; return false to decline. This example accepts only one seller and enforces a total budget of 1 SBC:

const budget = 1_000_000n; // 1 SBC in base units
let committed = 0n;
 
const payFetch = createRadiusFetch({
  network: 'testnet',
  signer: process.env.RADIUS_PRIVATE_KEY as `0x${string}`,
  maxPerRequest: '0.01 SBC',
  onPaymentRequired: (offer) => {
    if (offer.payTo !== '0xSellerAddress') return false;
    if (committed + BigInt(offer.amount) > budget) return false;
    committed += BigInt(offer.amount);
    return true;
  },
});

The offer also includes amountFormatted, resource.url, and scheme.

Handle payment errors

The client throws a RadiusPaymentError when it does not pay. Its code says why:

CodeMeaning
price_above_limitEvery compatible offer costs more than maxPerRequest
declinedonPaymentRequired returned false
network_mismatch, asset_mismatchThe server wants a different network or token
unsupported_transfer_methodThe server needs a transfer method other than permit2 or eip3009
payment_rejectedThe server answered the paid retry with another 402
approval_requiredA Permit2 approval is needed and permit2Approval is 'never'
import { RadiusPaymentError } from 'radius-sdk/client';
 
try {
  const response = await payFetch('https://{{SELLER_HOST}}/api/lookup');
} catch (error) {
  if (!(error instanceof RadiusPaymentError)) throw error;
  console.error(error.code, error.message);
}

For payment_rejected, error.details.response holds the server's response. See the reference for every code.

Confirm the payment

getPaymentReceipt decodes the seller's PAYMENT-RESPONSE header. It returns nothing for a free response:

import { getPaymentReceipt } from 'radius-sdk/client';
 
const receipt = getPaymentReceipt(response, payFetch.network);
if (receipt) console.log(receipt.transaction, receipt.amount, receipt.explorerUrl);

If a paid request fails or times out, check the chain before paying again. payFetch.getSettlement(txHash) returns the settlement's status and transfers, or undefined while the node does not know the transaction.

Permit2 approval

Payments on Radius use Permit2, which needs a one-time SBC approval. With the Radius facilitator, the approval is gas-sponsored: the client signs it together with the first payment, and no transaction is sent.

With a facilitator that does not sponsor it, the client sends one unlimited approval from the signer before the first payment. Its gas comes from SBC through the Turnstile, so keep about 0.01 SBC spare. To control this, set permit2Approval: 'never' or pass onApprovalRequired. Every payment is still a separate signature capped to its amount.

Pay from a terminal

For shell scripts and agents that run commands, radius-cli pays with the same client from a local keystore:

RADIUS_HOME=.radius RADIUS_NETWORK=testnet \
  radius-cli wallet x402 get https://{{SELLER_HOST}}/api/lookup \
  --x402-threshold 0.01 \
  --json \
  -y

--x402-threshold plays the role of maxPerRequest: with -y, the CLI refuses a more expensive offer instead of paying it.

Next steps