/ @prism-ing/swap-router

@prism-ing/swap-router

HTTP client SDK for the Prism meta-DEX aggregator service.

Install

pnpm add @prism-ing/swap-router @prism-ing/wallet

Aggregator 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 (SWAPS_XYZ_API_KEY and ZERODEV_PROJECT_ID).

Set baseUrl in your config to your instance URL (must include /v1).

Third-party API terms Anyone operating an aggregator-service instance integrates with swaps.xyz, ZeroDev, and (optionally) Fynd. You must comply with each provider's terms of service. See docs.swaps.xyz/resources/terms-of-service and zerodev.app/terms-of-service.

Solvers

The aggregator races available solvers on every quote. The best price wins.

Provider value Route type Notes
swaps-xyz Same-chain EVM, same-chain Solana, cross-chain bridge Always active
fynd Same-chain EVM (Ethereum, Base, Unichain) Optional server-side dependency

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('Tx hash:', execResult.value.txHash);
}

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

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 + broadcast via swaps.xyz bridge

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:   ['USDC:base', 'USDC:arbitrum'],
  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
  evmRpcUrls:  { 8453: process.env.BASE_RPC_URL },
});

// 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
PAYMASTER_FAILED ZeroDev paymaster rejected the UserOperation
SMART_ACCOUNT_NOT_DEPLOYED EVM smart account has no deployed bytecode
BUNDLER_ERROR ERC-4337 bundler rejected or reverted the op
SOLANA_SPONSORSHIP_FAILED ZeroDev Solana co-signing or broadcast failed
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;
  }
}

Gasless Execution

EVM — no gas, no separate approval

When a ZeroDev project is configured, token approval + swap are bundled into a single sponsored ERC-4337 UserOperation. The user's EOA needs no native token balance.

import { createSwapRouter, createZeroDevPaymaster } from '@prism-ing/swap-router';

const router = createSwapRouter({
  baseUrl: 'https://your-aggregator.fly.dev/v1',
  signer: wallet.signer,
  evmRpcUrls: { 8453: 'https://mainnet.base.org' },
  // Gasless EVM: all three fields required
  smartAccountAddress: '0xYourKernelAccountAddress',
  bundlerUrl: `https://rpc.zerodev.app/api/v2/bundler/${process.env.ZERODEV_PROJECT_ID}`,
  paymaster: createZeroDevPaymaster({ projectId: process.env.ZERODEV_PROJECT_ID! }),
});

The smart account must be a deployed ZeroDev Kernel v3.1 account. Counterfactual (undeployed) accounts return SMART_ACCOUNT_NOT_DEPLOYED.

Solana — no SOL fee

When a Solana paymaster is configured, ZeroDev co-signs as the transaction fee payer. Users pay zero SOL in transaction fees.

import { createSwapRouter, createZeroDevSolanaPaymaster } from '@prism-ing/swap-router';

const router = createSwapRouter({
  baseUrl: 'https://your-aggregator.fly.dev/v1',
  signer: wallet.signer,
  solanaRpcUrl: 'https://api.mainnet-beta.solana.com',
  solanaPaymaster: createZeroDevSolanaPaymaster({
    projectId: process.env.ZERODEV_PROJECT_ID!,
    network: 'mainnet', // or 'devnet'
  }),
});

quote() automatically fetches ZeroDev's fee payer address and injects it into aggregator requests. execute() calls ZeroDev's co-signing endpoint instead of broadcasting directly.

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
  // Gasless EVM (all three required together)
  smartAccountAddress?: string;                 // Deployed Kernel v3.1 account address
  bundlerUrl?: string;                          // ERC-4337 bundler URL
  paymaster?: PaymasterClient;                  // Use createZeroDevPaymaster()
  // Gasless Solana
  solanaPaymaster?: SolanaPaymasterClient;      // Use createZeroDevSolanaPaymaster()
}

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