Multi-chain settlement cost router for the x402 payment protocol. Selects the cheapest or fastest blockchain for each AI agent micropayment.
routex is an early-stage TypeScript project in the AI payments / x402 ecosystem, focused on agentic-payments, ai-agents, batch-settlement, blockchain. It currently has 2 GitHub stars and 0 forks, and sits alongside related tools like gold-402, piprail, x402charity.
Multi-chain settlement cost router for the x402 payment protocol. Selects the cheapest or fastest blockchain for each AI agent micropayment — non-custodial, stateless, and BigInt-safe.
Built for developers integrating x402 payments into AI agents, wallets, and autonomous services.
When an AI agent receives a 402 Payment Required response, it needs to pay — but which chain? Gas fees fluctuate, finality times vary, and balances are spread across networks. Routex evaluates all options in real time and picks the optimal chain automatically.
Real cost impact: A $0.10 USDC payment costs ~$0.001 on Base but ~$0.01 on Polygon — 10x difference. At 10,000 payments/day, that's $90/day saved by routing to the cheapest chain. Fees shift constantly; Routex checks every 30 seconds and picks the winner for each payment.
# Core router (required)
npm install @routexcc/core
# Chain adapters (install the ones you need)
npm install @routexcc/chain-base
npm install @routexcc/chain-stellar
npm install @routexcc/chain-solana
npm install @routexcc/chain-polygon
# x402 middleware (optional)
npm install @routexcc/x402
import { createRouter, LocalFeeOracle } from '@routexcc/core';
import { createBaseAdapter } from '@routexcc/chain-base';
import { createSolanaAdapter } from '@routexcc/chain-solana';
// 1. Configure chain adapters
const adapters = new Map([
['base', createBaseAdapter(viemClient)],
['solana', createSolanaAdapter(solanaConnection)],
]);
// 2. Create fee oracle
const feeOracle = new LocalFeeOracle({
adapters,
pollIntervalMs: 30_000,
maxFeeAgeMs: 60_000,
});
// 3. Create router
const router = createRouter({
adapters,
feeOracle,
strategy: 'cheapest',
maxFeeAgeMs: 60_000,
});
// 4. Route a payment
const result = await router.route(paymentRequirement, signer);
console.log(result.chainId); // 'base' or 'solana' — whichever is cheaper
console.log(result.payload); // Signed payload ready for the facilitator
Intercept 402 Payment Required responses and route payments automatically:
import { routexMiddleware } from '@routexcc/x402';
const middleware = routexMiddleware({
routeConfig: { adapters, feeOracle, strategy: 'cheapest', maxFeeAgeMs: 60_000 },
signer: mySigner,
onRouteSelected: (result) => console.log(`Paying via ${result.chainId}`),
onRouteFailed: (error) => console.error('Routing failed:', error),
});
// When you get a 402 response:
const parsed = middleware.parseResponse(402, responseBody);
if (parsed) {
const { payload, chainId } = await middleware.handlePaymentRequired(parsed);
// payload is ready to submit to the facilitator
}
| Strategy | Optimizes For | Best When |
|---|---|---|
'cheapest' |
Lowest fee in USD | Cost-sensitive workloads, high volume |
'fastest' |
Lowest finality time | Time-critical payments, real-time agents |
'balanced' |
60% cost / 40% speed | General-purpose, default choice |
custom |
Your scoring function | Domain-specific requirements |
const router = createRouter({
adapters,
feeOracle,
strategy: {
type: 'custom',
scorer: (options) => {
// Score and sort candidates however you want
return [...options].sort((a, b) => {
// Example: prefer Base, then sort by fee
if (a.chainId === 'base' && b.chainId !== 'base') return -1;
if (b.chainId === 'base' && a.chainId !== 'base') return 1;
return Number(a.fee.feeUsd - b.fee.feeUsd);
});
},
},
maxFeeAgeMs: 60_000,
});
interface RouteConfig {
adapters: Map<ChainId, ChainAdapter>; // Chain adapters to use
feeOracle: FeeOracle; // Fee estimation source
strategy: Strategy; // 'cheapest' | 'fastest' | 'balanced' | CustomStrategy
maxFeeAgeMs: number; // Reject fees older than this (ms)
maxFeeUsd?: bigint; // Max fee in USD (6-decimal bigint)
maxFinalityMs?: number; // Max acceptable finality time (ms)
excludeChains?: ChainId[]; // Chains to skip
}
const feeOracle = new LocalFeeOracle({
adapters, // Required — chain adapters to poll
pollIntervalMs: 30_000, // How often to refresh fees (default: 30s)
maxFeeAgeMs: 60_000, // When fees become stale (required)
fallbackAdapters: fallbacks, // Optional — for cross-validation
maxDivergencePercent: 50, // Flag low confidence if primary/fallback diverge
});
const router = createRouter({
adapters,
feeOracle,
strategy: 'cheapest',
maxFeeAgeMs: 60_000,
maxFeeUsd: 500000n, // Max $0.50 fee (6 decimals)
maxFinalityMs: 5000, // Max 5 seconds to finality
excludeChains: ['polygon'], // Skip Polygon
});
| Chain | Adapter Package | Finality | Typical Fee (USDC transfer) | Notes |
|---|---|---|---|---|
| Base (L2) | @routexcc/chain-base |
~2s | $0.0005–$0.005 | EVM, EIP-712 signing |
| Polygon | @routexcc/chain-polygon |
~2s | $0.005–$0.05 | EVM, shared base with Base |
| Stellar | @routexcc/chain-stellar |
~5s | $0.00001–$0.0001 | Stellar consensus |
| Solana | @routexcc/chain-solana |
~400ms | $0.0005–$0.005 | SPL token transfers |
All adapters implement the ChainAdapter interface:
interface ChainAdapter {
chainId: ChainId;
getBalance(address: string, token: string): Promise<TokenBalance>;
estimateFee(payment: AcceptedPayment): Promise<FeeEstimate>;
buildPaymentPayload(payment: AcceptedPayment, signer: Signer): Promise<PaymentPayload>;
getFinality(): number;
}
Routex uses a five-step pipeline for every route() call:
PaymentRequirement
|
1. Parse Extract candidates from accepted chains
|
2. Filter Remove ineligible candidates:
| - Chain excluded or no adapter
| - Fee stale, too high, or unavailable
| - Insufficient balance
| - Finality too slow
|
3. Score Apply routing strategy to remaining candidates
|
4. Select Pick highest-scoring candidate, build payload
|
5. Verify Enforce security invariants (recipient, amount, chain ID)
|
RouteResult { chainId, payload, fee, candidates }
If no candidates survive filtering, Routex throws a RouteExhaustedError with per-candidate rejection reasons — your code can then fall back to a direct payment.
Routex uses typed errors with full context — never silent failures.
import {
RouteExhaustedError,
StaleFeesError,
InsufficientBalanceError,
PaymentConstructionError,
} from '@routexcc/core';
try {
const result = await router.route(requirement, signer);
} catch (error) {
if (error instanceof RouteExhaustedError) {
// No chain eligible — check error.rejections for details
console.log(error.rejections);
// Fall back to direct payment on a specific chain
}
if (error instanceof InsufficientBalanceError) {
console.log(error.chainId, error.required, error.available);
}
if (error instanceof StaleFeesError) {
console.log(error.chainId, error.ageMs, error.maxAgeMs);
}
}
Routex is designed around ten security invariants enforced by code and automated tests:
| # | Invariant |
|---|---|
| INV-1 | No Routex function receives, stores, or returns a private key |
| INV-2 | Payment recipient matches the 402 response payTo address |
| INV-3 | Payment amount matches the 402 required amount (BigInt) |
| INV-4 | Chain ID in payload matches the selected adapter's chain |
| INV-5 | No eligible route throws RouteExhaustedError (never silently drops) |
| INV-6 | Fee estimates older than maxFeeAgeMs are rejected |
| INV-7 | All token arithmetic uses bigint — no floating-point |
| INV-8 | Error messages never contain credentials or RPC keys |
| INV-9 | Each route() call is stateless and independent |
| INV-10 | Router failure always allows caller to fall back to direct payment |
Routex interacts with your wallet through a Signer interface:
interface Signer {
sign(data: Uint8Array): Promise<Uint8Array>;
signTypedData?(domain: object, types: object, value: object): Promise<string>;
}
Routex calls .sign() or .signTypedData() to construct payment payloads. It never accesses .privateKey, .secretKey, or .mnemonic. If you grep the entire codebase for these strings, you'll find zero results in source files.
| Package | Description | npm |
|---|---|---|
@routexcc/core |
Router engine, strategies, fee oracle, types | |
@routexcc/chain-base |
Base (L2) chain adapter | |
@routexcc/chain-stellar |
Stellar chain adapter | |
@routexcc/chain-solana |
Solana chain adapter | |
@routexcc/chain-polygon |
Polygon chain adapter | |
@routexcc/x402 |
x402 middleware wrapper | |
@routexcc/cloud |
Cloud SDK (fee oracle, telemetry, batching) | coming soon |
Routex v1 works entirely local — zero cloud dependency. Routex Cloud adds an optional hosted layer:
All v1 code continues to work unchanged. Cloud features activate by setting a cloudApiKey in your config.
createRouter(config: RouteConfig): RouterCreates a new router instance.
const router = createRouter({
adapters,
feeOracle,
strategy: 'cheapest',
maxFeeAgeMs: 60_000,
});
Router.route(req: PaymentRequirement, signer: Signer): Promise<RouteResult>Evaluates all eligible chains and returns the best route with a signed payment payload.
const result = await router.route(paymentRequirement, signer);
// result.chainId — selected chain
// result.payload — signed PaymentPayload ready for submission
// result.fee — FeeEstimate for the selected chain
// result.candidates — all evaluated RouteOptions with scores
LocalFeeOraclePolls chain adapters for fee estimates. Caches results in memory with stale detection.
const oracle = new LocalFeeOracle({ adapters, pollIntervalMs: 30_000, maxFeeAgeMs: 60_000 });
oracle.start(); // Begin polling
const fee = await oracle.getFee('base');
const allFees = await oracle.getAllFees();
oracle.stop(); // Stop polling
BalanceManagerQueries token balances across all configured chains in parallel.
const balances = new BalanceManager({ adapters, cacheTtlMs: 15_000 });
const balanceMap = await balances.getBalances(walletAddress, tokenAddress);
// Map<ChainId, bigint>
type ChainId = 'base' | 'stellar' | 'solana' | 'polygon';
interface PaymentRequirement {
acceptedChains: AcceptedPayment[];
}
interface AcceptedPayment {
chainId: ChainId;
payTo: string;
amount: bigint;
token: string;
}
interface RouteResult {
chainId: ChainId;
payload: PaymentPayload;
fee: FeeEstimate;
candidates: readonly RouteOption[];
}
interface FeeEstimate {
feeAmount: bigint;
feeUsd: bigint; // 6-decimal fixed point
finalityMs: number;
confidence: 'high' | 'medium' | 'low';
}
interface RouteOption {
chainId: ChainId;
fee: FeeEstimate;
score: number;
eligible: boolean;
rejectionReason?: string;
}
require() and importRouteConfig option explainedClone the starter template to build an x402 agent with Routex in minutes:
git clone https://github.com/routexcc/x402-agent-starter.git
cd x402-agent-starter
pnpm install
cp .env.example .env
pnpm server # Terminal 1
pnpm agent # Terminal 2
See x402-agent-starter for the full template with environment config, non-custodial signer, and local test server.
git clone https://github.com/routexcc/routex.git
cd routex
pnpm install
pnpm build
pnpm test
We use changesets for versioning. Run pnpm changeset to create a changeset before submitting a PR.
⚡ The gold standard for x402 resources. 300+ projects, SDKs, tools, facilitators, and ecosystem data for the HTTP 402 Payment Required protocol. Curated by 24K Labs.
x402 (HTTP 402 Payment Required) SDK + MCP server: let any API charge for itself and any AI agent pay for itself, USDC & stablecoins across EVM, Solana & 8 more chain families, in a couple of lines. Backendless, no fee, self-custodial, paid straight to your wallet. TypeScript, MIT.
Automatic micro-donations powered by x402. Drop-in Express/Next.js middleware that sends USDC to charities on every user action. npm install x402charity.