# radius-sdk reference

*TypeScript SDK for x402 payments on Radius*

`radius-sdk` accepts and makes [x402 v2](/build/x402.md) payments on Radius. It defaults to SBC, the Radius facilitator, and mainnet. It can also pay and charge in USDC on Base or any other EVM network ([Networks](#networks)), and speak [MPP](#mpp) alongside x402. For task guides, see [Accept payments](/build/accept-payments.md) and [Make payments](/build/make-payments.md).

Source, changelog, and examples: [`radiustechsystems/tools/packages/sdk`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk). `radius-cli wallet pay` uses the same client.

> **Note:** `radius-sdk` is unrelated to the deprecated `@radiustechsystems/sdk`. It is pre-1.0: minor versions can change the API, so check the [changelog](https://github.com/radiustechsystems/tools/blob/main/packages/sdk/CHANGELOG.md) when you upgrade.

## Entry points

Requires Node.js 22 or later, or a runtime with `fetch` such as Cloudflare Workers.

| Entry point         | Exports                                                                                                                                                             | Peer dependency                                           |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `radius-sdk`        | Networks, `SBC`, `USDC_BASE`, price helpers, `getPaymentReceipt`, `RadiusPaymentError`, `radiusEnv`                                                                 | `@x402/core` `^2.27.0`                                    |
| `radius-sdk/server` | Web-standard `radiusPayments`, `createRadiusServer`, `radiusFacilitators`, `registerRadius`, `assertRadiusSupported`, `proxyTo`, `RequestAdapter`, and server types | `@x402/core` `^2.27.0`                                    |
| `radius-sdk/hono`   | `radiusPayments`, `proxyTo`, and their types                                                                                                                        | `@x402/core` `^2.27.0`, `hono` `^4`                       |
| `radius-sdk/client` | `createRadiusFetch`, balance, ERC-20 and Permit2 actions, `getSettlement`, and the receipt and error exports                                                        | `@x402/core` and `@x402/evm` `^2.27.0`, `viem` `^2.48.11` |

`@x402/core` is a required peer dependency: install it yourself (`pnpm add radius-sdk @x402/core`), and keep any upstream `@x402/*` adapters on the same version, so the SDK and your app share one copy. `@x402/evm` and viem are needed only for `radius-sdk/client`, and Hono only for `radius-sdk/hono`. The root, server, and Hono entry points do not load viem at runtime.

## radiusPayments

Answers unpaid requests to priced routes with `402 Payment Required`, then verifies and settles paid requests through a facilitator. Requests that match no route pass through.

From `radius-sdk/server`, `radiusPayments(options)` returns a web-standard handler for Workers, Bun, Deno, and framework route handlers that use `Request` and `Response`:

```typescript
import { radiusPayments } from 'radius-sdk/server';

const pay = radiusPayments({
  network: 'testnet',
  payTo: '0xYourWalletAddress',
  routes: { 'GET /api/lookup': '0.001 SBC' },
});

export default {
  fetch: pay.wrap((_request, payment) => Response.json({ paidBy: payment?.payer })),
};
```

| Member                         | Purpose                                                                        |
| ------------------------------ | ------------------------------------------------------------------------------ |
| `pay(request, next)`           | Handles payment, then calls `next(request, payment)` for an accepted request   |
| `pay.wrap(next)`               | Returns a `(request) => Promise<Response>` handler                             |
| `pay.requiresPayment(request)` | Checks whether a route is priced, without network I/O; does not verify payment |
| `pay.radius`                   | The underlying `RadiusServer`                                                  |

From `radius-sdk/hono`, it returns Hono middleware with the same payment options:

```typescript
import { radiusPayments } from 'radius-sdk/hono';

app.use('/api/*', radiusPayments({ network: 'testnet', payTo: '0xYourWalletAddress', routes: { 'GET /api/lookup': '0.001 SBC' } }));
```

| Option          | Type                                            | Default            | Description                                                                                                                                                        |
| --------------- | ----------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `payTo`         | address or `(context) => address`               | Required           | Recipient of every payment unless a route overrides it                                                                                                             |
| `routes`        | `Record<string, RouteSpec \| Price>`            | Required           | Priced routes keyed `METHOD /path`, with `*` wildcards; a bare price is shorthand for `{ price }`                                                                  |
| `network`       | A preset ID or a `PaymentNetwork`               | `'mainnet'`        | See [Networks](#networks)                                                                                                                                          |
| `networks`      | Array of networks or `{ network, facilitator }` | —                  | Instead of `network`: every route offers one payment option per network; see [Several networks](#several-networks)                                                 |
| `mpp`           | `{ secretKey, realm? }`                         | —                  | Also accept [MPP](#mpp) payments                                                                                                                                   |
| `settle`        | `'before'` or `'after'`                         | `'before'`         | `'before'` settles, then runs the handler, with no separate `/verify` call; `'after'` calls `/verify`, runs the handler, and settles only if it responds below 400 |
| `gasSponsoring` | `'auto'` or boolean                             | `'auto'`           | Declare `eip2612GasSponsoring` in 402s; `'auto'` follows the facilitator's `/supported`                                                                            |
| `facilitator`   | options object or `FacilitatorClient`           | Radius facilitator | See [Facilitator](#facilitator)                                                                                                                                    |
| `onSettled`     | `(receipt, context) => void \| Promise<void>`   | —                  | Called once per settled payment                                                                                                                                    |

It also accepts the [network overrides](#networks). Dynamic `payTo` and `price` functions receive the web-standard `Request` for `radius-sdk/server`, or the Hono context for `radius-sdk/hono`; they can return promises. `onSettled` receives that same context as its second argument.

With `settle: 'before'`, the web-standard handler receives the [receipt](#receipts) as its second argument. Hono handlers read `c.get('radiusPayment')`; add `RadiusPaymentVariables` to the Hono `Variables` type. With `settle: 'after'`, payment is not yet settled during the handler; use `onSettled` to record the receipt.

The SDK initializes the facilitator lazily on the first request matching a priced route, so creating either handler performs no module-scope I/O.

### Proxy to an origin

`proxyTo(origin)` returns a `(request) => Promise<Response>` function that forwards a request to an origin you do not change, keeping its method, path, and query. Use it as the paid handler, so paid requests reach the origin after settlement and requests that match no route pass through for free:

```typescript
import { proxyTo, radiusPayments } from 'radius-sdk/server';

const pay = radiusPayments({ network: 'testnet', payTo: '0xYourWalletAddress', routes: { 'POST /v1/score': '0.01 SBC' } });
export default { fetch: pay.wrap(proxyTo('http://127.0.0.1:8000')) };
```

With Hono, register `app.all('/*', (c) => toOrigin(c.req.raw))` after the middleware, where `toOrigin = proxyTo(origin)`. The request goes out without `Host`, hop-by-hop headers, and payment credentials (`PAYMENT-SIGNATURE`, `X-PAYMENT`, `Authorization: Payment`); other `Authorization` schemes pass through. A request body is buffered, and the origin is asked for an uncompressed response (`Accept-Encoding: identity`); if it compresses anyway, the decoded body goes out without `Content-Encoding`. A client that disconnects aborts the origin request. Redirects are returned, not followed.

### Settlement and streaming

The default `settle: 'before'` settles before the application handler runs and passes response bodies through without buffering, including SSE and token streams. A handler error or interrupted stream after settlement does not undo the payment. Validate inputs and check resource availability before entering the payment handler; see [validate before charging](/build/accept-payments.md#validate-before-charging).

`settle: 'after'` runs the handler first, buffers a successful response body, and settles before returning it. Responses with a status of 400 or higher are not charged. Use `'before'` for streaming responses.

`onSettled` records settlement, not delivery. With `'before'`, it runs before the application handler. Errors thrown by the hook are logged by x402 core and do not fail the request. Store the receipt and delivery outcome separately.

### RouteSpec

| Field               | Type                              | Default                  | Description                                   |
| ------------------- | --------------------------------- | ------------------------ | --------------------------------------------- |
| `price`             | `Price` or `(context) => Price`   | Required                 | See [Prices](#prices)                         |
| `payTo`             | address or `(context) => address` | The middleware's `payTo` | Recipient for this route                      |
| `description`       | string                            | —                        | Shown to buyers in the challenge's `resource` |
| `mimeType`          | string                            | —                        | Response type advertised in the challenge     |
| `maxTimeoutSeconds` | number                            | `300`                    | How long a signed payment stays valid         |

### Prices

A `Price` is a string or number in display units of the asset (`'0.001 SBC'`, `'0.001'`, `0.001`), or `{ amount: '1000' }` in base units. SBC has six decimals.

## createRadiusServer

`createRadiusServer(options)` from `radius-sdk/server` supplies the Radius resource server for upstream adapters such as `@x402/express`, `@x402/next`, and `@x402/hono`. It accepts `network`, `settle`, `gasSponsoring`, `facilitator`, `onSettled`, and the [network overrides](#networks). Configure recipients and prices through `routes()` or `handler()`.

| Member                              | Purpose                                                                                        |
| ----------------------------------- | ---------------------------------------------------------------------------------------------- |
| `radius.server`                     | The underlying x402 resource server, passed to an upstream adapter                             |
| `radius.routes({ payTo, routes })`  | Builds x402 `RoutesConfig`; dynamic price and recipient functions receive `HTTPRequestContext` |
| `radius.http({ payTo, routes })`    | Builds an `x402HTTPResourceServer` for adapters' `…FromHTTPServer` variants                    |
| `radius.handler({ payTo, routes })` | Builds the SDK's web-standard payment handler; dynamic functions receive `Request`             |
| `radius.onSettled(hook)`            | Registers a settlement listener and returns the server                                         |
| `radius.network`, `radius.networks` | The first resolved `PaymentNetwork`, and all of them in offer order                            |

For `createRadiusServer`, `onSettled(receipt, context)` receives the x402 request context, which can be `undefined`. `requestOf(context)` extracts the web-standard `Request` when an SDK adapter supplied it. `RequestAdapter` wraps a `Request` as an x402 `HTTPAdapter` for custom integrations.

Use upstream adapters on the same `@x402/*` version as the `@x402/core` you install for the SDK. They initialize the facilitator at construction by default. Where module-scope I/O is forbidden, pass the adapter's `syncFacilitatorOnStart: false` option and await `radius.server.initialize()` during request handling before invoking the adapter. The SDK's own web-standard and Hono handlers handle lazy initialization for you.

Upstream adapters serve x402 only; [MPP](#mpp) needs the SDK's own `radiusPayments` handler.

See [Express and upstream adapters](/build/accept-payments.md#express-and-upstream-adapters) for an example.

## Add Radius to an existing x402 server

When your application already owns an `x402ResourceServer` and drives verify, work, and settle itself, register Radius on it with three functions from `radius-sdk/server`. They take one options object:

```typescript
import { x402ResourceServer } from '@x402/core/server';
import { assertRadiusSupported, radiusFacilitators, registerRadius } from 'radius-sdk/server';

const radius = { networks: ['mainnet', 'testnet'], settle: 'after' } as const;

const server = registerRadius(new x402ResourceServer([...radiusFacilitators(radius), yourFacilitator]), radius);

await server.initialize();
assertRadiusSupported(server, radius);
```

| Option          | Type                                            | Default       | Description                                                |
| --------------- | ----------------------------------------------- | ------------- | ---------------------------------------------------------- |
| `settle`        | `'before'` or `'after'`                         | Required      | Decides whether x402 core calls `/verify`; see below       |
| `networks`      | Array of networks or `{ network, facilitator }` | `['mainnet']` | The networks to register; Base needs its own `facilitator` |
| `gasSponsoring` | `'auto'` or boolean                             | `'auto'`      | As for [`radiusPayments`](#radiuspayments)                 |

* `radiusFacilitators(options)` returns one facilitator client per network. Put them **first** in the list you pass to `new x402ResourceServer(...)`: core sends each network to the first facilitator that lists it, and `x402ResourceServer` cannot gain facilitators after it is built. Each client reports only its own network.
* `registerRadius(server, options)` registers the Radius `exact` scheme for each network and returns `server`.
* `assertRadiusSupported(server, options)` throws unless the facilitator from `radiusFacilitators` reported `exact` on every network. Call it after `server.initialize()`: core skips a facilitator it cannot reach while another answers, which would leave Radius offered in 402s but unable to settle, or settled by a facilitator you did not configure. A network answered by another facilitator in your list fails the check too.
* A facilitator call that gets no answer (a network error or a non-x402 error body) throws `FacilitatorResponseError`: the outcome is unknown, and a settle may have reached the chain. Answer with a 5xx, not as a rejected payment.

`settle` has no default here, because `'before'` skips the facilitator's `/verify`. With `'before'`, `server.verifyPayment()` returns `isValid: true` without checking the payment, and `/settle` does the validating, so settle before you do any work. With `'after'`, `verifyPayment()` calls `/verify` as a dry run, then you do the work, then `settlePayment()`. Use `'after'` for a verify, work, settle pipeline.

You build the routes and the 402 yourself in this setup, so `radiusPayments` options such as `mpp` and `onSettled` do not apply; register `server.onAfterSettle(...)` for settlement callbacks.

## createRadiusFetch

Returns a `fetch` that pays x402 and [MPP](#mpp) challenges and retries the request. Responses other than `402` are returned unchanged.

```typescript
import { createRadiusFetch } from 'radius-sdk/client';

const payFetch = createRadiusFetch({ network: 'testnet', signer: process.env.RADIUS_PRIVATE_KEY as `0x${string}`, maxPerRequest: '0.01 SBC' });
```

| Option               | Type                                         | Default            | Description                                                                                                                                                                  |
| -------------------- | -------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `signer`             | private key, viem account, or `WalletClient` | Required           | Signs payments; sending transactions needs a key, a local account, or a `WalletClient`                                                                                       |
| `maxPerRequest`      | `Price`                                      | Required           | Ceiling for each payment; not a cumulative budget                                                                                                                            |
| `network`            | A preset ID or a `PaymentNetwork`            | `'mainnet'`        | Payments are made only on this network                                                                                                                                       |
| `networks`           | Array of networks                            | —                  | Instead of `network`: pay on any of these, preferring them in order                                                                                                          |
| `protocols`          | Array of `'x402'` and `'mpp'`                | `['x402', 'mpp']`  | Protocols to pay with, in preference order                                                                                                                                   |
| `preferFunded`       | `boolean`                                    | `true`             | With several candidate networks, pay on one whose balance covers the price                                                                                                   |
| `onPaymentRequired`  | `(offer) => boolean \| Promise<boolean>`     | —                  | Approve or decline an [offer](#offers) before signing                                                                                                                        |
| `permit2Approval`    | `'auto'` or `'never'`                        | `'auto'`           | Without gas sponsoring, `'auto'` sends one unlimited Permit2 approval; `'never'` throws `approval_required`, also for a sponsored raw approve (`erc20ApprovalGasSponsoring`) |
| `onApprovalRequired` | `(request) => boolean \| Promise<boolean>`   | —                  | Approve or decline any allowance change this client makes; see [approval requests](#approval-requests)                                                                       |
| `onPaid`             | `(receipt, offer) => void`                   | —                  | Called after each paid response                                                                                                                                              |
| `fetch`              | `typeof fetch`                               | `globalThis.fetch` | Underlying fetch                                                                                                                                                             |

It also accepts the [network overrides](#networks); with `networks`, they apply to the first network.

Payment rules:

* Pays only on the configured networks, each in its payment asset.
* Takes the first compatible offer within `maxPerRequest`: protocols in your order, then networks in your order, then offers in the server's order. When the server accepts more than one of your networks, the client first reads the wallet's balance on each, and the first one that covers the price wins, whichever protocol offers it. If none does, it throws `insufficient_balance` before signing. Set `preferFunded: false` to skip the balance reads.
* A price in display units is converted for each network's asset; a base-unit `maxPerRequest` applies to each asset as-is.
* Schemes: x402 v2 `exact` (Permit2 or EIP-3009) and `upto` (Permit2), and x402 v1 `exact` (EIP-3009).
* The signing window is the server's `maxTimeoutSeconds`, capped at 600 seconds.
* A request whose 402 comes after a redirect is paid at the final URL, repeating the original method and body (a POST that fetch turned into a GET on a 301, 302 or 303 is paid as a POST). A redirect after payment is followed without payment headers, and the receipt is read from it.

### Offers

`onPaymentRequired` receives a `PaymentOffer`, either an x402 offer or an MPP offer:

| Field             | Description                                                                         |
| ----------------- | ----------------------------------------------------------------------------------- |
| `protocol`        | `'x402'` or `'mpp'`                                                                 |
| `amount`          | Base units as a string; for `upto`, the authorized maximum                          |
| `amountFormatted` | Display amount, for example `0.01 SBC`                                              |
| `payTo`, `asset`  | Recipient and token                                                                 |
| `network`         | The `PaymentNetwork` the offer pays on; `network.asset` has the symbol and decimals |
| `resource`        | `{ url, description, mimeType }` from the challenge                                 |
| `scheme`          | `exact` or `upto`; an MPP charge is `exact`                                         |
| `transferMethod`  | `permit2` or `eip3009`                                                              |
| `gasSponsored`    | Whether the facilitator sponsors the Permit2 approval                               |
| `x402Version`     | x402 offers: `1` or `2`                                                             |
| `requirements`    | x402 offers: the raw requirement from the challenge                                 |
| `challenge`       | MPP offers: the `WWW-Authenticate: Payment` challenge                               |

### Approval requests

`onApprovalRequired` receives an `ApprovalRequest` for every allowance change: the Permit2 approval a payment needs, `approvePermit2()`, and `approve()`. Return `false` to decline; the call throws `declined` with the request as `details`.

| Field              | Description                                                                                                                             |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `reason`           | `'payment'`, `'approvePermit2'`, or `'approve'`                                                                                         |
| `network`          | The `PaymentNetwork` the approval transaction is sent on                                                                                |
| `asset`, `spender` | Token and the address being approved                                                                                                    |
| `amount`           | Allowance to grant: unlimited for Permit2, the caller's amount for `approve`                                                            |
| `currentAllowance` | The spender's current allowance                                                                                                         |
| `offer`            | The [offer](#offers) that needs the approval; only for `'payment'`                                                                      |
| `sponsored`        | `'erc20Approval'` when the facilitator submits a raw approve this client signs (no transaction is sent by this client); unset otherwise |

### Wallet helpers

The returned function also has these members. The wallet helpers act on the first network; `on(network)` returns them for another one.

| Member                     | Returns                                                                                   |
| -------------------------- | ----------------------------------------------------------------------------------------- |
| `address`, `network`       | Signer address and the first resolved network                                             |
| `networks`                 | Every network this client pays on, in preference order                                    |
| `on(network)`              | The helpers below for one of `networks`, by preset ID, CAIP-2 ID, or `PaymentNetwork`     |
| `maxPerRequest`            | The cap in base units                                                                     |
| `balance()`                | `{ atomic, formatted }`: the signer's balance of the payment asset                        |
| `balances()`               | Native RUSD and SBC separately; see [Balances](#balances)                                 |
| `send(to, amount)`         | Transfers the payment asset; needs a key or local account                                 |
| `allowance(spender)`       | The payment-asset allowance granted to `spender`, in base units                           |
| `approve(spender, amount)` | Approves `spender` for `amount` of the payment asset; goes through `onApprovalRequired`   |
| `permit2Allowance()`       | The current Permit2 allowance                                                             |
| `approvePermit2()`         | Sends the unlimited Permit2 approval now; goes through `onApprovalRequired`               |
| `getSettlement(txHash)`    | A [settlement](#settlements), or `undefined` while the node does not know the transaction |
| `fund()`                   | Requests a faucet drip for this wallet (Radius networks)                                  |
| `client`                   | The underlying `@x402/core` client                                                        |

Members that send a transaction return a `TxResult`: `hash`, `status` (`'success'` or `'reverted'` from the receipt), and `explorerUrl`.

## Receipts

`getPaymentReceipt(response, network)` decodes a `PAYMENT-RESPONSE` (x402) or `Payment-Receipt` (MPP) header and returns `undefined` when there is none. With `settle: 'before'`, `radiusPayments` exposes the receipt as the web-standard handler's second argument or Hono's `c.get('radiusPayment')`.

| Field                         | Description                                                                                                                          |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `success`                     | Whether settlement succeeded                                                                                                         |
| `protocol`                    | `'x402'` or `'mpp'`                                                                                                                  |
| `transaction`                 | Settlement transaction hash                                                                                                          |
| `payer`, `network`            | Paying address and CAIP-2 network                                                                                                    |
| `amount`                      | Base units charged; for `upto` it can be less than the offer. `getPaymentReceipt` reports it only when the facilitator does (`upto`) |
| `explorerUrl`                 | Explorer link for the transaction                                                                                                    |
| `errorReason`, `errorMessage` | Set when settlement failed                                                                                                           |

## Settlements

`getSettlement(radiusTestnet, txHash)` (any `PaymentNetwork`) from `radius-sdk/client`, or `payFetch.getSettlement(txHash)`, reads a settlement transaction from the chain. The result has `status`, `blockNumber`, `transfers` (each `{ from, to, amount }` in the payment asset), `paid(to?)`, `paidFormatted(to?)`, and `explorerUrl`. Use `paid(to)` for what a recipient received: `transfers` lists every payment-asset transfer in the transaction, which can include the facilitator's fee conversion.

## Balances

On Radius, `eth_getBalance` includes convertible SBC; see [The Turnstile and balances](/reference/ethereum-compatibility.md#the-turnstile-and-balances). These viem actions from `radius-sdk/client` report each part separately:

| Action                                        | Returns                                                                              |
| --------------------------------------------- | ------------------------------------------------------------------------------------ |
| `getBalances(client, { address })`            | `native` (`raw`, `aggregate`, `convertible`), `tokens` (SBC by default), and `total` |
| `getNativeBalance(client, { address })`       | Native RUSD only, in wei                                                             |
| `getAggregateBalance(client, { address })`    | What `eth_getBalance` returns                                                        |
| `getTokenBalance(client, { address, token })` | An ERC-20 `balanceOf`                                                                |

Each takes an optional `blockNumber` or `blockTag`. `radiusActions()` adds them to a viem client:

```typescript
import { createPublicClient, http } from 'viem';
import { radiusTestnet } from 'radius-sdk';
import { radiusActions } from 'radius-sdk/client';

const client = createPublicClient({ chain: radiusTestnet.chain, transport: http() }).extend(radiusActions());
const balances = await client.getBalances({ address: '0xYourWalletAddress' });
```

## ERC-20 actions

`erc20Actions({ token?, network? })` from `radius-sdk/client` adds token actions to a viem client. The token defaults to the payment asset of the client's chain when it is a preset (SBC on Radius, USDC on Base); on any other chain, name the `token`. Amounts are base units as `bigint` or display units as a string, for example `'0.01'`.

| Action                                                   | Returns                                                                                                                                                                 |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getTokenMetadata()`                                     | `address`, `name`, `symbol`, `decimals`, and `totalSupply`                                                                                                              |
| `getAllowance({ owner, spender })`                       | Allowance in base units                                                                                                                                                 |
| `approve({ spender, amount })`                           | `TxResult`                                                                                                                                                              |
| `transfer({ to, amount })`                               | `TxResult`                                                                                                                                                              |
| `transferFrom({ from, to, amount })`                     | `TxResult`                                                                                                                                                              |
| `getTransfers({ from?, to?, fromBlock?, toBlock? })`     | Transfer events; defaults to the last `MAX_LOG_RANGE` (1,000,000) blocks, and splits wider ranges                                                                       |
| `watchTransfers({ from?, to?, fromBlock?, onTransfer })` | A stop function; polls for transfers, awaits delivery and checkpoint persistence; resume from a durable cursor and deduplicate by chain ID plus `transferKey(transfer)` |

Writes wait for the receipt. Pass `wait: false` to return as soon as the transaction is sent, with `status: 'pending'`.

```typescript
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { radiusTestnet } from 'radius-sdk';
import { erc20Actions } from 'radius-sdk/client';

const wallet = createWalletClient({
  chain: radiusTestnet.chain,
  transport: http(),
  account: privateKeyToAccount(process.env.RADIUS_PRIVATE_KEY as `0x${string}`),
}).extend(erc20Actions());

const result = await wallet.transfer({ to: '0xRecipientAddress', amount: '0.01' });
console.log(result.status, result.explorerUrl);
```

Block numbers on Radius are millisecond timestamps, so 1,000,000 blocks is about 17 minutes; see [`eth_getLogs`](/reference/ethereum-compatibility.md#eth_getlogs-constraints).

`watchTransfers` queries up to `head - 5000n` on Radius mainnet and testnet: a five-second visibility delay, separate from transaction finality. `blockLag` overrides this nonnegative number of block-number units; it defaults to zero on other chains. Clients without a chain must set it explicitly for Radius. Without `fromBlock`, watching begins after the first observed head and waits for the lagged boundary to reach it. `getTransfers` remains an immediate snapshot unless you pass a lagged `toBlock`.

The watcher awaits each `onTransfer` and then `onCheckpoint` before advancing to the next chunk. Return the persistence promise from both callbacks. A thrown error or rejected promise retries the chunk, including previously delivered transfers. Stopping prevents further delivery but cannot cancel an in-flight callback.

A checkpoint is the inclusive end of a successfully processed query, **not a completeness watermark**. Receipts and logs can appear after their timestamp, and five seconds is not a guaranteed maximum delay. Use this ingestion contract:

1. Persist each event and its downstream effect atomically, with a unique key of `(chainId, transactionHash, logIndex)`. `transferKey` supplies only the last two fields; namespace it by chain. Do not key on block number or transaction index.
2. Persist the checkpoint only after all effects are durable. Store cursors separately for each chain, token and filter configuration. Restart at the persisted checkpoint plus one to retry unacknowledged work, or earlier for overlap.
3. Periodically re-scan an overlapping historical window with `getTransfers`, using the same durable deduplication. Choose the window from measured visibility delays and recovery requirements. Keep replay progress separate from the forward cursor. A finite window cannot recover arbitrarily late logs; reconcile older intervals as needed.
4. Track a separate verified-through watermark only for intervals reconciled against an authoritative source with confirmed coverage. See [receipt archive backfill](/reference/ethereum-compatibility.md#receipt-archive-backfill-and-verification) for availability limits.

Delivery is replayable, not exactly once. Sorting is within each chunk by block number and log index; replay may deliver older events later, and these fields do not establish global execution order on Radius.

## Permit2 actions

`permit2Actions()` from `radius-sdk/client` adds actions for the canonical Permit2 contract. x402 payments use them internally; use them directly to build your own Permit2 flows.

| Action                                                                                 | Purpose                                                                        |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `getPermit2Approval({ owner })`                                                        | The ERC-20 allowance the owner has granted Permit2                             |
| `approvePermit2()`                                                                     | Grants Permit2 an unlimited allowance (once per token)                         |
| `signPermit2Transfer({ amount, spender, witness? })`                                   | Signs a one-off transfer off-chain (SignatureTransfer, as x402 uses)           |
| `permit2TransferFrom({ signed, to })`                                                  | The spender submits a signed transfer to pull the tokens                       |
| `isPermit2NonceUsed({ owner, nonce })`                                                 | Whether a SignatureTransfer nonce has been used                                |
| `signPermit2Allowance(...)`, `permit2Permit(...)`, `permit2AllowanceTransferFrom(...)` | AllowanceTransfer: a time-limited allowance the spender can draw on repeatedly |
| `getPermit2Allowance({ owner, spender })`                                              | The current AllowanceTransfer allowance and its expiration                     |

Writes return a `TxResult`. The EIP-712 helpers and ABI (`permit2Domain`, `PERMIT2_ABI`, and the type definitions) are exported too.

## MPP

[MPP](https://mpp.dev), the Machine Payments Protocol, is the HTTP `Payment` authentication scheme. `radius-sdk` speaks its `evm` charge method only (`method="evm"`, `intent="charge"`). Like x402's `exact` scheme with EIP-3009, an MPP charge is an EIP-3009 authorization of the network's stablecoin, and the same facilitator settles it. Only the HTTP envelope differs:

| Step       | x402                                     | MPP                                                      |
| ---------- | ---------------------------------------- | -------------------------------------------------------- |
| 402        | `PAYMENT-REQUIRED` (offers in `accepts`) | `WWW-Authenticate: Payment …`, one challenge per network |
| Paid retry | `PAYMENT-SIGNATURE`                      | `Authorization: Payment …`                               |
| Receipt    | `PAYMENT-RESPONSE`                       | `Payment-Receipt`                                        |

**Buyers.** `createRadiusFetch` pays both. When a 402 offers both, `protocols` sets the preference, x402 first by default. MPP payments need no approval and no gas. A request that already sends its own `Authorization` header is not paid over MPP, because the credential goes in that header.

**Sellers.** Add `mpp` to `radiusPayments`:

```typescript
const pay = radiusPayments({
  network: 'testnet',
  payTo: '0xYourWalletAddress',
  routes: { 'GET /api/lookup': '0.001 SBC' },
  mpp: { secretKey: env.MPP_SECRET_KEY },
});
```

Every 402 then also carries an MPP challenge per network. It mirrors the x402 offers that use EIP-3009, with the same price, asset, recipient, and expiry. The challenge ID is an HMAC of the challenge under `secretKey`, so credentials are checked without stored state. Use at least 32 random characters (`openssl rand -base64 32`), and the same key on every instance that serves the routes.

This is not a drop-in for a server whose MPP rail is Tempo through `mppx`. A Tempo challenge has `method="tempo"` and a different `request`; the SDK never emits one, and `createRadiusFetch` rejects it with `no_compatible_offer`. To accept Radius on such a server, add Radius over x402 ([above](#add-radius-to-an-existing-x402-server)), or add `mppx`'s own `evm` method alongside Tempo.

The SDK checks the realm (`mpp.realm`, by default the request's host), the expiry, the route's current price, and the authorization's nonce, which is bound to the challenge. Then the facilitator verifies and settles the payment, before or after the handler as `settle` says. The handler's receipt and `onSettled` have `protocol: 'mpp'`, and the response carries `Payment-Receipt`.

## Errors

### Seller responses

The SDK's web-standard and Hono handlers return `502` with `error: 'facilitator_error'` when a facilitator call fails without the facilitator's own answer, such as a dropped connection or gateway error page. Settlement can already have reached the chain. A `502` does not mean nothing moved: [reconcile the attempt](/build/x402.md#recover-from-failed-or-uncertain-payments) before authorizing another payment. A facilitator's explicit payment rejection still returns `402`.

### Buyer errors

Payment failures throw `RadiusPaymentError` with a `code` and, for some codes, `details`:

| Code                          | Cause                                                                                                         |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `price_above_limit`           | Every compatible offer is above `maxPerRequest`                                                               |
| `insufficient_balance`        | The server accepts several of your networks and the wallet's balance covers the price on none                 |
| `declined`                    | `onPaymentRequired` or `onApprovalRequired` returned `false`                                                  |
| `network_mismatch`            | The server's offers are on networks this client does not pay on                                               |
| `asset_mismatch`              | The server's offers are in another token                                                                      |
| `no_compatible_offer`         | No offer uses a scheme or MPP method this client pays                                                         |
| `unsupported_transfer_method` | The server needs a transfer method other than `permit2` or `eip3009`                                          |
| `invalid_challenge`           | The 402 could not be parsed; `details.response` holds the response                                            |
| `payment_rejected`            | The paid retry got another 402; `details.response` holds the response and `details.error` the server's reason |
| `invalid_receipt`             | An `upto` payment response is invalid, for example it charges more than the signed maximum                    |
| `redirect_refused`            | No longer raised: a redirect on the paid request is followed without the payment header                       |
| `approval_required`           | A Permit2 approval is needed and `permit2Approval` is `'never'`                                               |
| `approval_failed`             | The approval transaction reverted                                                                             |
| `faucet`                      | `fund()` failed                                                                                               |
| `config`                      | The options are invalid, for example a missing `maxPerRequest`                                                |

## Networks

| Network                          | Chain ID | Asset | Default facilitator | Export          |
| -------------------------------- | -------- | ----- | ------------------- | --------------- |
| `'mainnet'` / `'radius'`         | 723487   | SBC   | Radius              | `radiusMainnet` |
| `'testnet'` / `'radius-testnet'` | 72344    | SBC   | Radius              | `radiusTestnet` |
| `'base'`                         | 8453     | USDC  | None: name one      | `baseMainnet`   |
| `'base-sepolia'`                 | 84532    | USDC  | x402.org            | `baseSepolia`   |

A `PaymentNetwork` carries a viem `Chain` (`radiusTestnet.chain`) plus its payment `asset`, default `facilitatorUrl`, `faucetUrl` (Radius only), and the names x402 v1 challenges use for it. `definePaymentNetwork({ chain, asset, facilitatorUrl })` builds one for any other EVM network; `asset` gives the token's address, symbol, decimals, and EIP-712 domain `name` and `version`.

Both `radiusPayments` and `createRadiusFetch` accept these overrides alongside `network`:

| Override                   | Default                                                   |
| -------------------------- | --------------------------------------------------------- |
| `rpcUrl`                   | The network's public RPC endpoint                         |
| `facilitatorUrl`           | The network's default facilitator                         |
| `asset`                    | The network's payment asset (SBC on Radius, USDC on Base) |
| `explorerUrl`, `faucetUrl` | The network's explorer and faucet                         |

Prices in display units assume a USD stablecoin, which every preset uses.

### Several networks

`networks` makes a seller offer one payment option per network on every route, in that order. Each network is settled by its own facilitator. Base mainnet has no default facilitator, so name one:

```typescript
const pay = radiusPayments({
  networks: ['mainnet', { network: 'base', facilitator: { url: 'https://your-base-facilitator.example' } }],
  payTo: '0xYourWalletAddress',
  routes: { 'GET /api/lookup': '0.001' },
});
```

The same `payTo` receives on every network. A price in display units is converted for each network's asset. A price that names an asset with `{ amount, asset }` is rejected when there are several networks. Receipts report the network a payment settled on. With `networks`, top-level `facilitator` and overrides apply to the first network.

### Facilitator

For `radiusPayments` and `createRadiusServer`, `facilitator` takes:

* `{ url, apiKey }` for another hosted facilitator; `apiKey` is sent as `x-api-key`
* `{ live: false }` to use the built-in Radius `/supported` answer instead of fetching it during initialization (Radius networks only)
* `{ timeoutMs }` to bound facilitator calls
* a `FacilitatorClient` from `@x402/core/server` for a self-hosted facilitator

The seller offers whichever of `eip3009` and `permit2` the facilitator lists first for `exact`, and `eip3009` when it names none. The Radius facilitators list `eip3009` first. This does not add EIP-3009 support to a token: the configured asset must support the selected transfer method.

## Environment variables

`radiusEnv(env)` reads these variables, using the same names as `radius-cli`, and returns options to spread into either function. Explicit options take precedence. On Cloudflare Workers, pass `c.env`.

| Variable                     | Option                                       |
| ---------------------------- | -------------------------------------------- |
| `RADIUS_NETWORK`             | `network` (a preset ID; default `mainnet`)   |
| `RADIUS_RPC_URL`             | `rpcUrl`                                     |
| `RADIUS_FACILITATOR_URL`     | `facilitatorUrl`                             |
| `RADIUS_FACILITATOR_API_KEY` | `facilitator.apiKey`                         |
| `RADIUS_ASSET_ADDRESS`       | `asset.address` (alias `RADIUS_SBC_ADDRESS`) |
| `RADIUS_PAY_TO`              | `payTo`                                      |
| `RADIUS_PRIVATE_KEY`         | `signer`                                     |
| `RADIUS_MAX_PER_REQUEST`     | `maxPerRequest`                              |

## Examples

| Example                                                                                                       | What it shows                                                           |
| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| [`worker-plain`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk/examples/worker-plain)     | Worker using `radius-sdk/server` without a framework                    |
| [`express-seller`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk/examples/express-seller) | Express with the upstream x402 adapter                                  |
| [`astro-seller`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk/examples/astro-seller)     | Astro API route that validates before charging                          |
| [`worker-seller`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk/examples/worker-seller)   | Hono Worker with a free route and two paid routes                       |
| [`agent-buyer`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk/examples/agent-buyer)       | Node scripts that pay a URL, including from a wallet holding only SBC   |
| [`demo-dapp`](https://github.com/radiustechsystems/tools/tree/main/packages/sdk/examples/demo-dapp)           | Browser page that exercises both sides with a burner wallet or MetaMask |

## Related pages

* [Accept payments](/build/accept-payments.md)
* [Make payments](/build/make-payments.md)
* [x402 payments](/build/x402.md)
* [radius-cli](/reference/radius-cli.md)
* [x402 facilitator API](/reference/facilitator-api.md)
