Skip to content
LogoLogo

radius-sdk reference

TypeScript SDK for x402 payments on Radius

View as Markdown

radius-sdk accepts and makes x402 v2 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), and speak MPP alongside x402. For task guides, see Accept payments and Make payments.

Source, changelog, and examples: radiustechsystems/tools/packages/sdk. radius-cli wallet pay uses the same client.

Entry points

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

Entry pointExportsPeer dependency
radius-sdkNetworks, SBC, USDC_BASE, price helpers, getPaymentReceipt, RadiusPaymentError, radiusEnv@x402/core ^2.27.0
radius-sdk/serverWeb-standard radiusPayments, createRadiusServer, radiusFacilitators, registerRadius, assertRadiusSupported, proxyTo, RequestAdapter, and server types@x402/core ^2.27.0
radius-sdk/honoradiusPayments, proxyTo, and their types@x402/core ^2.27.0, hono ^4
radius-sdk/clientcreateRadiusFetch, 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:

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 })),
};
MemberPurpose
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.radiusThe underlying RadiusServer

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

import { radiusPayments } from 'radius-sdk/hono';
 
app.use('/api/*', radiusPayments({ network: 'testnet', payTo: '0xYourWalletAddress', routes: { 'GET /api/lookup': '0.001 SBC' } }));
OptionTypeDefaultDescription
payToaddress or (context) => addressRequiredRecipient of every payment unless a route overrides it
routesRecord<string, RouteSpec | Price>RequiredPriced routes keyed METHOD /path, with * wildcards; a bare price is shorthand for { price }
networkA preset ID or a PaymentNetwork'mainnet'See Networks
networksArray of networks or { network, facilitator }—Instead of network: every route offers one payment option per network; see Several networks
mpp{ secretKey, realm? }—Also accept 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
facilitatoroptions object or FacilitatorClientRadius facilitatorSee Facilitator
onSettled(receipt, context) => void | Promise<void>—Called once per settled payment

It also accepts the network overrides. 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 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:

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.

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

FieldTypeDefaultDescription
pricePrice or (context) => PriceRequiredSee Prices
payToaddress or (context) => addressThe middleware's payToRecipient for this route
descriptionstring—Shown to buyers in the challenge's resource
mimeTypestring—Response type advertised in the challenge
maxTimeoutSecondsnumber300How 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. Configure recipients and prices through routes() or handler().

MemberPurpose
radius.serverThe 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.networksThe 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 needs the SDK's own radiusPayments handler.

See 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:

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);
OptionTypeDefaultDescription
settle'before' or 'after'RequiredDecides whether x402 core calls /verify; see below
networksArray of networks or { network, facilitator }['mainnet']The networks to register; Base needs its own facilitator
gasSponsoring'auto' or boolean'auto'As for 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 challenges and retries the request. Responses other than 402 are returned unchanged.

import { createRadiusFetch } from 'radius-sdk/client';
 
const payFetch = createRadiusFetch({ network: 'testnet', signer: process.env.RADIUS_PRIVATE_KEY as `0x${string}`, maxPerRequest: '0.01 SBC' });
OptionTypeDefaultDescription
signerprivate key, viem account, or WalletClientRequiredSigns payments; sending transactions needs a key, a local account, or a WalletClient
maxPerRequestPriceRequiredCeiling for each payment; not a cumulative budget
networkA preset ID or a PaymentNetwork'mainnet'Payments are made only on this network
networksArray of networks—Instead of network: pay on any of these, preferring them in order
protocolsArray of 'x402' and 'mpp'['x402', 'mpp']Protocols to pay with, in preference order
preferFundedbooleantrueWith several candidate networks, pay on one whose balance covers the price
onPaymentRequired(offer) => boolean | Promise<boolean>—Approve or decline an offer 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
onPaid(receipt, offer) => void—Called after each paid response
fetchtypeof fetchglobalThis.fetchUnderlying fetch

It also accepts the network overrides; 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:

FieldDescription
protocol'x402' or 'mpp'
amountBase units as a string; for upto, the authorized maximum
amountFormattedDisplay amount, for example 0.01 SBC
payTo, assetRecipient and token
networkThe PaymentNetwork the offer pays on; network.asset has the symbol and decimals
resource{ url, description, mimeType } from the challenge
schemeexact or upto; an MPP charge is exact
transferMethodpermit2 or eip3009
gasSponsoredWhether the facilitator sponsors the Permit2 approval
x402Versionx402 offers: 1 or 2
requirementsx402 offers: the raw requirement from the challenge
challengeMPP 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.

FieldDescription
reason'payment', 'approvePermit2', or 'approve'
networkThe PaymentNetwork the approval transaction is sent on
asset, spenderToken and the address being approved
amountAllowance to grant: unlimited for Permit2, the caller's amount for approve
currentAllowanceThe spender's current allowance
offerThe offer 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.

MemberReturns
address, networkSigner address and the first resolved network
networksEvery 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
maxPerRequestThe cap in base units
balance(){ atomic, formatted }: the signer's balance of the payment asset
balances()Native RUSD and SBC separately; see 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, or undefined while the node does not know the transaction
fund()Requests a faucet drip for this wallet (Radius networks)
clientThe 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').

FieldDescription
successWhether settlement succeeded
protocol'x402' or 'mpp'
transactionSettlement transaction hash
payer, networkPaying address and CAIP-2 network
amountBase units charged; for upto it can be less than the offer. getPaymentReceipt reports it only when the facilitator does (upto)
explorerUrlExplorer link for the transaction
errorReason, errorMessageSet 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. These viem actions from radius-sdk/client report each part separately:

ActionReturns
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:

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

ActionReturns
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'.

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.

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

ActionPurpose
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, 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:

Stepx402MPP
402PAYMENT-REQUIRED (offers in accepts)WWW-Authenticate: Payment …, one challenge per network
Paid retryPAYMENT-SIGNATUREAuthorization: Payment …
ReceiptPAYMENT-RESPONSEPayment-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:

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), 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 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:

CodeCause
price_above_limitEvery compatible offer is above maxPerRequest
insufficient_balanceThe server accepts several of your networks and the wallet's balance covers the price on none
declinedonPaymentRequired or onApprovalRequired returned false
network_mismatchThe server's offers are on networks this client does not pay on
asset_mismatchThe server's offers are in another token
no_compatible_offerNo offer uses a scheme or MPP method this client pays
unsupported_transfer_methodThe server needs a transfer method other than permit2 or eip3009
invalid_challengeThe 402 could not be parsed; details.response holds the response
payment_rejectedThe paid retry got another 402; details.response holds the response and details.error the server's reason
invalid_receiptAn upto payment response is invalid, for example it charges more than the signed maximum
redirect_refusedNo longer raised: a redirect on the paid request is followed without the payment header
approval_requiredA Permit2 approval is needed and permit2Approval is 'never'
approval_failedThe approval transaction reverted
faucetfund() failed
configThe options are invalid, for example a missing maxPerRequest

Networks

NetworkChain IDAssetDefault facilitatorExport
'mainnet' / 'radius'723487SBCRadiusradiusMainnet
'testnet' / 'radius-testnet'72344SBCRadiusradiusTestnet
'base'8453USDCNone: name onebaseMainnet
'base-sepolia'84532USDCx402.orgbaseSepolia

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:

OverrideDefault
rpcUrlThe network's public RPC endpoint
facilitatorUrlThe network's default facilitator
assetThe network's payment asset (SBC on Radius, USDC on Base)
explorerUrl, faucetUrlThe 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:

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.

VariableOption
RADIUS_NETWORKnetwork (a preset ID; default mainnet)
RADIUS_RPC_URLrpcUrl
RADIUS_FACILITATOR_URLfacilitatorUrl
RADIUS_FACILITATOR_API_KEYfacilitator.apiKey
RADIUS_ASSET_ADDRESSasset.address (alias RADIUS_SBC_ADDRESS)
RADIUS_PAY_TOpayTo
RADIUS_PRIVATE_KEYsigner
RADIUS_MAX_PER_REQUESTmaxPerRequest

Examples

ExampleWhat it shows
worker-plainWorker using radius-sdk/server without a framework
express-sellerExpress with the upstream x402 adapter
astro-sellerAstro API route that validates before charging
worker-sellerHono Worker with a free route and two paid routes
agent-buyerNode scripts that pay a URL, including from a wallet holding only SBC
demo-dappBrowser page that exercises both sides with a burner wallet or MetaMask