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. For task guides, see Accept payments and Make payments.

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

Entry points

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

Entry pointExportsPeer dependency
radius-sdkNetworks, SBC, price helpers, getPaymentReceipt, RadiusPaymentError, radiusEnv—
radius-sdk/honoradiusPayments and its typeshono ^4
radius-sdk/clientcreateRadiusFetch, balance, ERC-20 and Permit2 actions, getSettlement, and the receipt and error exportsviem ^2.48

The root and Hono entry points do not load viem at runtime.

radiusPayments

Hono middleware that 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.

import { radiusPayments } from 'radius-sdk/hono';
 
app.use('/api/*', radiusPayments({ network: 'testnet', payTo: '0xYourWalletAddress', routes: { 'GET /api/lookup': '0.001 SBC' } }));
OptionTypeDefaultDescription
payToaddress or (c) => 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 }
network'mainnet', 'testnet', or a RadiusNetwork'mainnet'See Networks
settle'before' or 'after''before''before' settles, then runs the handler; 'after' 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, c) => void—Called once per settled payment

It also accepts the network overrides. In handlers, c.get('radiusPayment') holds the receipt; add RadiusPaymentVariables to your Hono Variables type to type it.

RouteSpec

FieldTypeDefaultDescription
pricePrice or (c) => PriceRequiredSee Prices
payToaddress or (c) => 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.

createRadiusFetch

Returns a fetch that pays x402 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
network'mainnet', 'testnet', or a RadiusNetwork'mainnet'Payments are made only on this network
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
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.

Payment rules:

  • Pays only on the configured network and in the configured asset.
  • Takes the first compatible offer within maxPerRequest, in the server's order.
  • 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.
  • The paid retry never follows a redirect to another origin (redirect_refused).

Offers

onPaymentRequired receives a PaymentOffer:

FieldDescription
amountBase units as a string; for upto, the authorized maximum
amountFormattedDisplay amount, for example 0.01 SBC
payTo, asset, networkRecipient, token, and CAIP-2 network
resource{ url, description, mimeType } from the challenge
scheme, x402Versionexact or upto; 1 or 2
transferMethodpermit2 or eip3009
gasSponsoredWhether the facilitator sponsors the Permit2 approval
requirementsThe raw requirement from the 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'
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'

Wallet helpers

The returned function also has these members:

MemberReturns
address, networkSigner address and resolved network
maxPerRequestThe cap in base units
balance(){ atomic, formatted }: the signer's SBC balance
balances()Native RUSD and SBC separately; see Balances
send(to, amount)Transfers SBC; needs a key or local account
allowance(spender)The SBC allowance granted to spender, in base units
approve(spender, amount)Approves spender for amount of SBC; 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
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 header and returns undefined when there is none. radiusPayments exposes the same object as c.get('radiusPayment').

FieldDescription
successWhether settlement succeeded
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 RadiusNetwork) 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 (SBC) of the client's Radius chain; 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 new transfers in order, at least once each; resume with fromBlock set to the block from onCheckpoint plus one, and deduplicate with 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.

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.

Errors

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

CodeCause
price_above_limitEvery compatible offer is above maxPerRequest
declinedonPaymentRequired or onApprovalRequired returned false
network_mismatchThe server's offers are on another network
asset_mismatchThe server's offers are in another token
no_compatible_offerNo offer uses a scheme 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
invalid_receiptAn upto payment response is invalid, for example it charges more than the signed maximum
redirect_refusedThe paid retry redirected to another origin
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 IDExport
'mainnet'723487radiusMainnet
'testnet'72344radiusTestnet

A RadiusNetwork carries a viem Chain (radiusTestnet.chain) plus facilitatorUrl, faucetUrl, and asset. defineRadiusNetwork builds one from a chain ID or a viem chain.

Both radiusPayments and createRadiusFetch accept these overrides alongside network:

OverrideDefault
rpcUrlThe network's public RPC endpoint
facilitatorUrlThe Radius facilitator for the network
assetSBC (six decimals, permit domain Stable Coin version 1)
explorerUrl, faucetUrlThe network's explorer and faucet

Facilitator

For radiusPayments, 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 on the first paid request after each cold start
  • { timeoutMs } to bound facilitator calls
  • a FacilitatorClient from @x402/core/server for a self-hosted facilitator

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 (mainnet or testnet)
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-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