Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Real-time API Metering

Problem statement

Conventional API billing relies on monthly invoices processed with credit card rails. In practice, this means API providers are extending credit for services rendered for 30+ days and while also paying 2.9% +$0.30 in transaction fees. For agents procuring services in real-time for sporadic, bursty workloads, this model is insufficient.

Radius solves this with real-time, per-request billing. Each API call includes payment that settles instantly. No credit card fees. No counterparty risk. No intermediaries.

How it works

When a client calls your API:

  1. Client sends payment proof — The client constructs a micro-transaction on Radius and includes proof in the API request
  2. Server verifies payment — Your API server verifies the payment on Radius in milliseconds
  3. Request executes — If payment is valid, your API processes the request
  4. Instant settlement — The payment is finalized within seconds, and you control the tokens immediately

Total latency: sub-second verification + your API response time. No batch processes. No waiting for settlement.

Implementation example

Below is a complete, production-ready example: an Express.js API that charges per request using Radius.

Server setup

Create a new project:

mkdir radius-api && cd radius-api
pnpm init

Add "type": "module" to package.json:

{
  "name": "radius-api",
  "type": "module",
  "dependencies": {
    "express": "^4.18.2",
    "@radiustechsystems/sdk": "latest",
    "viem": "latest"
  }
}

Install dependencies:

pnpm add express @radiustechsystems/sdk viem

Server implementation

Create api-server.ts:

import express, { Request, Response } from 'express';
import { createPublicClient, createWalletClient, http, parseEther, isAddress } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { radiusTestnet, radiusWalletActions } from '@radiustechsystems/sdk';
import type { Address, Hash } from 'viem';
 
// Create account from server's private key
const serverAccount = privateKeyToAccount(
  process.env.SERVER_PRIVATE_KEY as `0x${string}`
);
 
// Create clients
const publicClient = createPublicClient({
  chain: radiusTestnet,
  transport: http(),
});
 
const walletClient = createWalletClient({
  account: serverAccount,
  chain: radiusTestnet,
  transport: http(),
}).extend(radiusWalletActions());
 
// Configuration
const COST_PER_REQUEST = parseEther('0.001'); // 0.001 USD per request
 
const app = express();
app.use(express.json());
 
/**
 * Verify that a payment transaction was sent by the client
 * Returns the sender's address if valid, null otherwise
 */
async function verifyPayment(
  transactionHash: Hash,
  expectedAmount: bigint,
  expectedRecipient: Address
): Promise<Address | null> {
  try {
    const receipt = await publicClient.waitForTransactionReceipt({
      hash: transactionHash,
    });
 
    // Check transaction success
    if (receipt.status !== 'success') {
      return null;
    }
 
    // Get transaction details
    const tx = await publicClient.getTransaction({
      hash: transactionHash,
    });
 
    // Verify recipient and amount
    if (
      !tx.to ||
      tx.to.toLowerCase() !== expectedRecipient.toLowerCase() ||
      tx.value < expectedAmount
    ) {
      return null;
    }
 
    // Payment is valid
    return tx.from;
  } catch (error) {
    console.error('Payment verification failed:', error);
    return null;
  }
}
 
/**
 * Protected API endpoint that requires payment
 */
app.post('/api/query', async (req: Request, res: Response) => {
  const { paymentHash, query } = req.body;
 
  if (!paymentHash || !query) {
    return res.status(400).json({
      error: 'Missing paymentHash or query',
    });
  }
 
  // Verify the payment
  const payer = await verifyPayment(
    paymentHash as Hash,
    COST_PER_REQUEST,
    serverAccount.address
  );
 
  if (!payer) {
    return res.status(402).json({
      error: 'Payment verification failed or insufficient amount',
    });
  }
 
  // Log the payment for your records
  console.log(`Query from ${payer}: ${query}`);
 
  // Process the actual API request
  // (In a real app, this would be your business logic)
  const result = {
    query,
    result: `Processing query: "${query}"`,
    processedAt: new Date().toISOString(),
    paidBy: payer,
  };
 
  return res.json({
    success: true,
    data: result,
  });
});
 
/**
 * Health check endpoint (no payment required)
 */
app.get('/health', (_req: Request, res: Response) => {
  res.json({ status: 'ok', serverAddress: serverAccount.address });
});
 
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`API server running on http://localhost:${PORT}`);
  console.log(`Server receives payments at: ${serverAccount.address}`);
});

Client implementation

Create api-client.ts:

import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { radiusTestnet, radiusWalletActions } from '@radiustechsystems/sdk';
import type { Address } from 'viem';
 
// Client configuration
const apiServerUrl = 'http://localhost:3000';
const serverAddress: Address = process.env.SERVER_ADDRESS as `0x${string}`;
const clientPrivateKey = process.env.CLIENT_PRIVATE_KEY as `0x${string}`;
 
// Create client account
const clientAccount = privateKeyToAccount(clientPrivateKey);
 
// Create clients
const publicClient = createPublicClient({
  chain: radiusTestnet,
  transport: http(),
});
 
const walletClient = createWalletClient({
  account: clientAccount,
  chain: radiusTestnet,
  transport: http(),
}).extend(radiusWalletActions());
 
const COST_PER_REQUEST = parseEther('0.001');
 
/**
 * Make a metered API call
 * 1. Send payment to the server
 * 2. Use the transaction hash as proof of payment
 * 3. Call the API with the payment proof
 */
async function callMeteredAPI(query: string): Promise<void> {
  console.log(`\nCalling API with query: "${query}"`);
 
  // Step 1: Send payment to the server
  console.log('Sending payment...');
  const paymentHash = await walletClient.sendTransaction({
    to: serverAddress,
    value: COST_PER_REQUEST,
  });
 
  console.log(`Payment sent: ${paymentHash}`);
 
  // Step 2: Call the API with the payment proof
  console.log('Calling API endpoint...');
  const response = await fetch(`${apiServerUrl}/api/query`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      paymentHash,
      query,
    }),
  });
 
  if (!response.ok) {
    const error = await response.json();
    console.error(`API error (${response.status}):`, error);
    return;
  }
 
  const result = await response.json();
  console.log('API response:', result.data);
}
 
// Example usage
async function main() {
  try {
    // Check client balance
    const balance = await publicClient.getBalance({
      address: clientAccount.address,
    });
    console.log(`Client balance: ${balance.toString()} wei`);
 
    // Make a few metered API calls
    await callMeteredAPI('What is 2 + 2?');
    await callMeteredAPI('What is the capital of France?');
  } catch (error) {
    console.error('Error:', error);
  }
}
 
main();

Running the example

Setup environment

Create .env:

# Server wallet (receives payments)
SERVER_PRIVATE_KEY=0x...
 
# Client wallet (sends payments)
CLIENT_PRIVATE_KEY=0x...
 
# For client: server's address (where to send payments)
SERVER_ADDRESS=0x...

Get testnet tokens from the Radius Faucet for both wallets.

Run server

node --env-file=.env --import=tsx api-server.ts

You'll see:

API server running on http://localhost:3000
Server receives payments at: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

Run client

In another terminal:

node --env-file=.env --import=tsx api-client.ts

You'll see:

Client balance: 10000000000000000000 wei
 
Calling API with query: "What is 2 + 2?"
Payment sent: 0xabcd...
Calling API endpoint...
API response: { query: 'What is 2 + 2?', ... }
 
Calling API with query: "What is the capital of France?"
Payment sent: 0xdef0...
Calling API endpoint...
API response: { query: 'What is the capital of France?', ... }

Real-world implementation notes

Payment verification

In production, consider:

  • Nonce tracking — Store processed transaction hashes to prevent replay attacks
  • Timeout handling — If a transaction takes too long to finalize, retry or fail gracefully
  • Rate limiting — Limit requests per wallet to prevent spam
  • Amount validation — Verify the payment amount exactly matches your pricing

Pricing strategies

// Fixed rate per request
const COST_PER_REQUEST = parseEther('0.001');
 
// Tiered pricing based on request type
const PRICING = {
  basic: parseEther('0.001'),
  premium: parseEther('0.005'),
  enterprise: parseEther('0.01'),
};
 
// Per-token pricing for AI/ML APIs
const COST_PER_TOKEN = parseEther('0.000001');

Monitoring and analytics

Track payments to understand your revenue:

interface PaymentRecord {
  timestamp: Date;
  payer: Address;
  amount: bigint;
  query: string;
  transactionHash: string;
}
 
const payments: PaymentRecord[] = [];
 
// Record each successful payment
payments.push({
  timestamp: new Date(),
  payer,
  amount: COST_PER_REQUEST,
  query,
  transactionHash: paymentHash,
});

Benefits over traditional API billing

FeatureTraditionalRadius
Payment fees2.9% + 0.30 USD~0.000001 USD per transfer
Settlement time30+ daysSeconds
ChargebacksCommon, costlyImpossible (on-chain)
Global accessCredit card requiredWallet + USD only
Minimum transaction5–10 USD0.0001 USD
Revenue controlIntermediary takes a cutYou control 100%

Use cases

AI/ML APIs

Charge per inference or per token. Users pay only for what they use, instantly:

const costPerToken = parseEther('0.000001'); // Per token charged
const tokensGenerated = 150;
const totalCost = BigInt(tokensGenerated) * costPerToken;
 
// Client pays before using the API
const hash = await walletClient.sendTransaction({
  to: apiServer,
  value: totalCost,
});

Premium data feeds

Real-time stock prices, weather data, sports stats—charge per request:

app.post('/api/stock-price', async (req, res) => {
  const { symbol, paymentHash } = req.body;
 
  // Verify payment for premium data
  const payer = await verifyPayment(
    paymentHash,
    PREMIUM_DATA_COST,
    serverAccount.address
  );
 
  if (!payer) {
    return res.status(402).json({ error: 'Payment required' });
  }
 
  // Return premium stock price data
  res.json({ symbol, price: 123.45, timestamp: Date.now() });
});

Content APIs

Charge for access to paywalled articles, ebooks, or videos:

// Client pays for each piece of content
const contentId = '123-article-slug';
const hash = await walletClient.sendTransaction({
  to: publisherAddress,
  value: ARTICLE_COST,
});
 
// Request with proof of payment
const content = await fetch('/api/articles/' + contentId, {
  headers: { 'X-Payment-Hash': hash },
});

Best practices

  • Show pricing upfront — Let users know the cost before sending payment
  • Batch requests — Allow clients to send multiple queries in one payment
  • Discounts for volume — Offer lower per-request costs for bulk prepayment
  • Error handling — Handle failed payments gracefully; return 402 Payment Required
  • Monitoring — Track payment success rates and processing times

FAQ

How long does payment verification take?

Sub-second. Radius achieves near-instant finality, so payment verification completes in milliseconds.

Can clients batch multiple requests into one payment?

Yes. You can structure pricing around request batches or data volume rather than individual calls.

What if a payment transaction fails?

The transaction fails on-chain with no side effects. Your API returns 402 Payment Required, and the client can retry.

Do I need a smart contract?

No. Native token transfers work perfectly. Only use smart contracts if you need complex payment logic (for example, split payments, conditional refunds).

Can I refund payments?

Yes, by sending tokens back to the client. You control the server wallet and can execute refund transactions directly.

How do I handle pricing changes?

Update your server code and redeploy. Clients see the new pricing immediately. Consider a grace period for old prices.

Next steps

  • Quick Start — Send your first transaction on Radius
  • JSON-RPC API — Low-level RPC reference for advanced use cases