@stablecoin.xyz/radius-mpp
v0.1.5
Published
MPP custom payment method for Radius blockchain — ERC-20 token transfers
Readme
@stablecoin.xyz/radius-mpp
MPP custom payment method for Radius blockchain ERC-20 token transfers.
Built on the mppx SDK using the Method.from / Method.toClient / Method.toServer pattern.
Install
npm install @stablecoin.xyz/radius-mpp mppx viemClient usage
The client signs and broadcasts an ERC-20 transfer() on Radius, waits for confirmation, then returns the txHash as the credential payload.
import { Mppx, radius } from '@stablecoin.xyz/radius-mpp/client'
import { radiusMainnet } from '@stablecoin.xyz/radius-mpp'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount('0x...')
const walletClient = createWalletClient({
account,
chain: radiusMainnet,
transport: http(radiusMainnet.rpcUrls.default.http[0]),
})
const { fetch } = Mppx.create({
methods: [radius({ walletClient })],
polyfill: false,
})
// Any 402 response from an MPP server using the "radius" method
// will be handled automatically
const response = await fetch('https://api.example.com/premium-endpoint')Server usage
The server verifies that the txHash corresponds to a successful ERC-20 transfer on Radius with the correct amount, token, and recipient. Import Mppx from the framework adapter you're using (mppx/hono, mppx/express, mppx/nextjs, or mppx/elysia).
import { Mppx } from 'mppx/hono'
import { Expires } from 'mppx/server'
import { radius } from '@stablecoin.xyz/radius-mpp/server'
import { SBC_TOKEN, radiusMainnet } from '@stablecoin.xyz/radius-mpp'
const mppx = Mppx.create({
methods: [radius({ confirmations: 1 })],
})
// Hono example — gate a route behind a 1 SBC payment
app.get(
'/premium',
mppx.charge({
amount: '1000000', // 1 SBC (6 decimals)
currency: 'SBC',
chainId: radiusMainnet.id,
token: SBC_TOKEN.mainnet,
recipient: '0xYourAddress',
expires: Expires.minutes(5),
}),
(c) => c.json({ data: 'premium content' }),
)Server options
radius() accepts a config object passed to Mppx.create:
const mppx = Mppx.create({
methods: [radius({
confirmations: 1, // optional
rpcUrl: 'https://...', // optional
})],
})| Option | Default | Description |
|--------|---------|-------------|
| confirmations | 1 | How many block confirmations to wait before accepting the payment. Radius has fast finality so 1 is usually sufficient. Increase for high-value transactions where you want extra certainty the tx won't reorg. |
| rpcUrl | Chain's default RPC | Override the RPC endpoint used to fetch and verify transaction receipts. Useful if you're running your own Radius node or need a higher-throughput RPC provider. |
All options are optional — radius() with no arguments works out of the box using chain defaults.
Payer enforcement
By default, any wallet can pay for a gated endpoint. If you need to restrict payments to a specific wallet (e.g. the authenticated user's wallet), pass payer in the charge options:
app.get(
'/premium',
mppx.charge({
amount: '1000000',
currency: 'SBC',
chainId: radiusMainnet.id,
token: SBC_TOKEN.mainnet,
recipient: MERCHANT_WALLET,
payer: userSession.walletAddress, // only accept payment from this wallet
expires: Expires.minutes(5),
}),
handler,
)When payer is set, the server verifies that receipt.from matches the specified address. If someone else's wallet pays, the server rejects the payment. When payer is omitted, any wallet can pay.
See examples/ for complete working examples with Hono, Express, Next.js, and Elysia.
Radius chain specifics (as of Mar 31, 2026)
Fixed gas pricing. Radius uses a fixed gas price (~1 gwei) with priority fee always 0. The client sets maxFeePerGas from eth_gasPrice and maxPriorityFeePerGas: 0. An explicit gas limit (160k) skips eth_estimateGas, which fails on mainnet when the wallet relies on the Turnstile for RUSD gas conversion.
The Turnstile. Radius automatically converts SBC → RUSD (1:1, zero-fee) when a wallet lacks RUSD for gas but holds SBC. This means agents only need SBC — the protocol covers gas. Minimum conversion is 0.1 SBC, which covers ~10,000 transfers. The conversion is one-way (SBC → RUSD, not reversible).
Instant finality. When waitForTransactionReceipt returns on Radius, the transaction is final. There are no reorgs. confirmations: 1 achieves full finality.
Supported chains
| Network | Chain ID | RPC | Explorer |
|---------|----------|-----|----------|
| Radius Mainnet | 723487 | https://rpc.radiustech.xyz | network.radiustech.xyz |
| Radius Testnet | 72344 | https://rpc.testnet.radiustech.xyz | testnet.radiustech.xyz |
Legacy chain ID 723 is aliased to mainnet for backwards compatibility.
Explorer helper
import { explorerTxUrl } from '@stablecoin.xyz/radius-mpp'
const url = explorerTxUrl(72344, '0xee483...')
// → "https://testnet.radiustech.xyz/tx/0xee483..."Token
| Token | Address | Decimals |
|-------|---------|----------|
| SBC | 0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb | 6 |
Same contract address on both mainnet and testnet.
Examples
Self-contained in examples/ with their own package.json. To run:
cd examples
pnpm install
MPP_SECRET_KEY=your-secret RECIPIENT=0xYourAddress pnpm hono| Script | Framework | Description |
|--------|-----------|-------------|
| pnpm hono | Hono | Single paid endpoint |
| pnpm express | Express | Express middleware |
| pnpm testnet | Hono | Testnet configuration |
| pnpm multi-tier | Hono | Multiple price tiers |
| pnpm client | — | Client auto-payment (NETWORK=testnet by default) |
| nextjs-route.ts | Next.js | App Router route handler (copy into your app) |
| elysia-server.ts | Elysia | Bun/Elysia guard pattern |
Environment variables
| Variable | Used by | Description |
|----------|---------|-------------|
| MPP_SECRET_KEY | Server | HMAC signing key for challenges |
| RECIPIENT | Server | Wallet address to receive SBC payments |
| PRIVATE_KEY | Client | Hex-encoded wallet private key |
| API_URL | Client | Server URL (default: http://localhost:3000) |
| NETWORK | Client | mainnet or testnet (default: testnet) |
Development
pnpm build # Build with tsup (ESM → dist/)
pnpm dev # Build in watch mode
pnpm typecheck # tsc --noEmit
pnpm test # Run all tests
pnpm test:coverage # Tests with coverage thresholds
pnpm test:unit # Unit tests only
pnpm test:integration # Integration tests onlyPre-commit hook runs typecheck → build → tests with coverage enforcement. Installed automatically via pnpm prepare.
Coverage thresholds
| Metric | Threshold | |--------|-----------| | Lines | 90% | | Branches | 85% | | Functions | 90% | | Statements | 90% |
License
MIT
