@prism-ing/swap-router
v1.0.4
Published
HTTP client SDK for the Prism aggregator service — quotes, signing, and execution for EVM, Solana, and cross-chain swaps.
Maintainers
Readme
@prism-ing/swap-router
HTTP client SDK for the Prism meta-DEX aggregator service.
Install
pnpm add @prism-ing/swap-router @prism-ing/walletAggregator Service
@prism-ing/swap-router is an HTTP client for the Prism aggregator service. You need a running instance.
Self-host: Deploy apps/aggregator-service to Fly.io — see the aggregator README for required environment variables.
Set baseUrl in your config to your instance URL (must include /v1).
Third-party API terms Anyone operating an aggregator-service instance integrates with Jupiter, KyberSwap, OKX DEX, swaps.xyz, and Fynd. You must comply with each provider's terms of service. Notably, Jupiter's SDK License Agreement requires that end-user interfaces display "Powered by Jupiter" attribution.
Solvers
The aggregator races all available solvers on every quote. The best price wins.
| Provider value | Route type | Notes |
|----------------|-----------|-------|
| kyberswap | Same-chain EVM | High liquidity on Base, Arbitrum, Polygon |
| jupiter | Same-chain Solana | Best for Solana routes |
| okx-dex | Same-chain EVM or Solana | Alternative across both ecosystems |
| swaps-xyz | Same-chain or cross-chain bridge | Handles bridge routes as fallback |
| onebalance | Cross-chain fast-path | Requires accounts field with smart account |
| fynd | Same-chain EVM | Optional server-side dependency |
| bf-evm | Same-chain EVM | Bellman-Ford optimizer, optional dep |
| bf-solana | Same-chain Solana | Bellman-Ford optimizer, optional dep |
Pin to a specific solver for testing with the provider field in QuoteRequest.
Quickstart
import { createSwapRouter } from '@prism-ing/swap-router';
const router = createSwapRouter({
baseUrl: 'https://your-aggregator.fly.dev/v1',
// Optional: provide a signer to enable execution (not just quoting)
signer: mySigner,
evmRpcUrls: { 8453: 'https://mainnet.base.org' },
solanaRpcUrl: 'https://api.mainnet-beta.solana.com',
});
// Get the best swap quote from all solvers
const quoteResult = await router.quote({
fromAsset: 'USDC:base',
toAsset: 'WETH:base',
amount: '1000000', // 1 USDC (6 decimals)
sender: '0xYourAddress',
});
if (!quoteResult.ok) {
console.error(quoteResult.error.code);
process.exit(1);
}
const quote = quoteResult.value;
console.log(
`${quote.provider} → ${quote.netAmountOut} (impact: ${quote.priceImpactBps}bps)`,
);
// Execute the swap
const execResult = await router.execute(quote);
if (execResult.ok) {
console.log('Status:', execResult.value.status);
// For cross-chain: poll until COMPLETED
if (execResult.value.status === 'pending') {
const status = await router.getStatus(execResult.value.quoteId!);
console.log(status.ok ? status.value.status : status.error.code);
}
}Signer Interface
Any PrismSigner from @prism-ing/wallet satisfies SwapSigner — pass wallet.signer directly:
import { createProductionWallet } from '@prism-ing/wallet';
const walletResult = await createProductionWallet({ signer: { walletName: 'my-agent' } });
const wallet = walletResult.value;
const router = createSwapRouter({
baseUrl: '...',
signer: wallet.signer, // PrismSigner → SwapSigner, no cast needed
});signEvmTxHash is optional in the interface but required at runtime for EVM swap execution. All built-in PrismSigner backends implement it.
Asset Formats
| Format | Example | Notes |
| --------------------- | -------------------- | ------------------------------------- |
| Symbol + chain | "USDC:base" | Resolved to address by the aggregator |
| Address + chainId | "0xA0b86991c...:1" | Specific token address on chain |
| Solana symbol | "SOL:solana" | Native SOL |
| OneBalance aggregated | "ob:usdc" | Cross-chain, any chain |
Custom tokens: For tokens not in the aggregator's registry, use
"0xContractAddress:chainId"(e.g."0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48:1"for USDC on mainnet). Symbol + chain resolution only works for tokens the aggregator knows about.
Route Types
| Route | When | Execution |
| ------------------- | ----------------------------- | ----------------------------------------------- |
| same-chain-evm | Both assets on same EVM chain | Sign + broadcast via EVM RPC |
| same-chain-solana | Both assets on Solana | Sign + broadcast via Solana RPC |
| cross-chain | Assets on different chains | Sign OneBalance operations → poll getStatus() |
Cross-Chain with OneBalance
For cross-chain swaps, pass smart account info in the accounts field. MultiAccount from @prism-ing/onebalance is assignable to SwapAccount directly:
import type { MultiAccount } from '@prism-ing/onebalance';
const accounts: MultiAccount[] = [
{
type: 'kernel-v3.1-ecdsa',
signerAddress: wallet.evmAddress,
accountAddress: wallet.smartAccountAddress,
},
];
const quote = await router.quote({
fromAsset: 'USDC:base',
toAsset: 'USDT:arbitrum',
amount: '1000000',
sender: wallet.evmAddress,
accounts,
});See @prism-ing/onebalance for the full three-package bootstrap example.
Full Cross-Chain Polling Loop
For production use, poll until COMPLETED rather than relying on the auto-poll in execute():
import { createSwapRouter } from '@prism-ing/swap-router';
const router = createSwapRouter({
baseUrl: 'https://your-aggregator.fly.dev/v1',
signer: wallet.signer,
evmRpcUrls: { 8453: process.env.BASE_RPC_URL },
});
// Quote
const quoteResult = await router.quote({
fromAsset: 'USDC:base',
toAsset: 'USDC:arbitrum',
amount: '5000000',
sender: wallet.evmAddress,
accounts: [{ type: 'kernel-v3.1-ecdsa', accountAddress: wallet.smartAccountAddress, signerAddress: wallet.evmAddress }],
});
if (!quoteResult.ok) throw new Error(quoteResult.error.code);
// Execute (submit only — don't auto-poll)
const execResult = await router.execute(quoteResult.value);
if (!execResult.ok) throw new Error(execResult.error.code);
const exec = execResult.value;
if (exec.status === 'confirmed') {
console.log('Same-chain confirmed:', exec.txHash);
process.exit(0);
}
// Cross-chain — poll manually
const quoteId = exec.quoteId ?? '';
const INTERVAL = 5_000;
const TIMEOUT = 5 * 60_000;
const start = Date.now();
while (Date.now() - start < TIMEOUT) {
await new Promise((r) => setTimeout(r, INTERVAL));
const s = await router.getStatus(quoteId);
if (!s.ok) throw new Error(s.error.code);
console.log(`[${Math.round((Date.now() - start) / 1000)}s]`, s.value.status);
if (s.value.status === 'COMPLETED') {
console.log('Done! Tx:', s.value.txHash);
break;
}
if (s.value.status === 'FAILED') throw new Error('Cross-chain swap failed');
}Session Keys + Swaps
Pass a session key signer instead of the root signer for policy-scoped execution:
import { createSessionKeyManager } from '@prism-ing/wallet';
import { createSwapRouter } from '@prism-ing/swap-router';
// Create a session scoped to USDC only, 1-hour expiry, 10 USDC max per op
const manager = createSessionKeyManager(wallet.signer);
const session = manager.createSessionKey({
expiresAt: Math.floor(Date.now() / 1000) + 3600,
allowedAssets: ['ob:usdc'],
maxAmountPerOp: '10000000', // 10 USDC
allowedChains: [8453, 42161],
});
// The session signer satisfies SwapSigner directly
const router = createSwapRouter({
baseUrl: 'https://your-aggregator.fly.dev/v1',
signer: session.signer, // policy-scoped — can only sign USDC ops under 10 USDC
evmRpcUrls: { 8453: process.env.BASE_RPC_URL },
});
// If the agent tries to sign a 20 USDC swap, session.signer returns SESSION_KEY_POLICY_VIOLATION
const result = await router.execute(quote);
// Revoke when the agent task is done
manager.revokeSessionKey(session.sessionId);Error Handling
All methods return Result<T, SwapRouterError>. Error codes:
| Code | Meaning |
| ------------------ | ------------------------------------ |
| QUOTE_EXPIRED | Quote TTL elapsed before execution |
| SIGNER_REQUIRED | Execution called without a signer |
| EVM_RPC_REQUIRED | No RPC URL for the swap's chain |
| SIGNING_FAILED | Signer returned an error |
| BROADCAST_FAILED | Transaction rejected by the network |
| VALIDATION_ERROR | Unexpected aggregator response shape |
| NETWORK_ERROR | HTTP or connection failure |
| TIMEOUT | Request exceeded timeoutMs |
| QUOTE_NOT_FOUND | Status polled for unknown quoteId |
const result = await router.execute(quote);
if (!result.ok) {
switch (result.error.code) {
case 'QUOTE_EXPIRED':
// Re-fetch a fresh quote
break;
case 'SIGNER_REQUIRED':
// Provide a signer in SwapRouterConfig
break;
case 'NETWORK_ERROR':
// Retry — transient
break;
}
}Configuration
interface SwapRouterConfig {
baseUrl: string; // Aggregator URL (include /v1)
timeoutMs?: number; // Default: 10_000
signer?: SwapSigner; // Required for execution
evmRpcUrls?: Partial<Record<number, string>>; // Per-chain RPC endpoints
solanaRpcUrl?: string; // Solana RPC endpoint
approvalConfirmationTimeoutMs?: number; // ERC-20 approval wait. Default: 30_000
}Info Endpoints
const tokens = await router.getTokens(); // Token[]
const chains = await router.getChains(); // Chain[]Use these to discover what assets and chains are available on the connected aggregator.
License
MIT
