npm package: @stablecoin.xyz/radius-mpp
Description: MPP custom payment method for Radius blockchain — ERC-20 token transfers
Latest version: 0.2.2

# @stablecoin.xyz/radius-mpp

MPP custom payment method for **Radius blockchain** ERC-20 token transfers.

Built on the [mppx](https://github.com/wevm/mppx) SDK using the `Method.from` / `Method.toClient` / `Method.toServer` pattern.

## Install

```bash
npm install @stablecoin.xyz/radius-mpp mppx viem
```

## Client usage

The client signs and broadcasts an ERC-20 `transfer()` on Radius, waits for confirmation, then returns the `txHash` as the credential payload.

```ts
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')
```

### Standalone credential creation

For platforms that need control over the payment flow (e.g. checking spending limits between the 402 challenge and payment), use `createCredential` directly instead of the auto-fetch wrapper:

```ts
import { createCredential, type MppChallenge } from '@stablecoin.xyz/radius-mpp/client'

// Parse the WWW-Authenticate header from the 402 response
const challenge: MppChallenge = {
  id: 'challenge-id',
  method: 'radius',
  intent: 'charge',
  request: {
    amount: '1000000',     // 1 SBC in atomic units
    currency: 'SBC',
    chainId: 723487,
    token: '0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb',
    recipient: '0xSellerWallet',
  },
}

// ... check spending limits, verify balance, etc. ...

const { authorization, txHash } = await createCredential(walletClient, challenge)

// Retry the seller with the credential
const paidRes = await fetch(sellerUrl, {
  headers: { Authorization: authorization },
})
```

`createCredential` handles the ERC-20 transfer broadcast, Radius gas quirks, receipt confirmation, and credential serialization. You get back the `Authorization` header value ready to send and the `txHash` for your records.

#### Credential format

The credential is a base64url-encoded JSON object. Note that `challenge.request` is itself base64url-encoded (matching the mppx wire format):

```json
{
  "challenge": {
    "id": "challenge-id",
    "method": "radius",
    "intent": "charge",
    "request": "eyJhbW91bnQiOiIxMDAwMDAwIiwiY3VycmVuY3kiOiJTQkMiLC4uLn0"
  },
  "payload": {
    "txHash": "0xabc123..."
  }
}
```

Where `challenge.request` decodes to: `{ "amount": "1000000", "currency": "SBC", "chainId": 723487, "token": "0x...", "recipient": "0x..." }`

The `Authorization` header value is `Payment <base64url-credential>`.

## 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`).

```ts
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`:

```ts
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:

```ts
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/`](./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](https://docs.radiustech.xyz/developer-resources/ethereum-compatibility#the-turnstile-and-balances) 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](https://network.radiustech.xyz) |
| Radius Testnet | 72344 | `https://rpc.testnet.radiustech.xyz` | [testnet.radiustech.xyz](https://testnet.radiustech.xyz) |

Legacy chain ID `723` is aliased to mainnet for backwards compatibility.

### Explorer helper

```ts
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:

```bash
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`](./examples/nextjs-route.ts) | Next.js | App Router route handler (copy into your app) |
| [`elysia-server.ts`](./examples/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

```bash
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 only
```

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