Skip to main content

Overview

Use this cookbook when you want to integrate directly with the deployed contracts instead of relying on transaction-building endpoints. This gives you full control over:
  • token approvals,
  • router call parameters,
  • wallet UX,
  • and transaction submission.
The examples below use viem, which is also the library used in the current frontend stack.

Prerequisites

npm install viem
You will also need:
  • the target router address,
  • the source-chain USDC address,
  • the instrument ID,
  • and a connected wallet client.
See Contract Addresses for the deployed addresses.

Shared Setup

import {
  createPublicClient,
  createWalletClient,
  custom,
  http,
  parseUnits,
} from 'viem';
import { base, arbitrum } from 'viem/chains';

const ERC20_ABI = [
  {
    name: 'approve',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ name: '', type: 'bool' }],
  },
] as const;

const ROUTER_ABI = [
  {
    name: 'buy',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'instrumentId', type: 'bytes32' },
      { name: 'amount', type: 'uint256' },
      { name: 'minDepositedAmount', type: 'uint256' },
      { name: 'fastTransfer', type: 'bool' },
      { name: 'maxFee', type: 'uint256' },
    ],
    outputs: [{ name: 'depositedAmount', type: 'uint256' }],
  },
  {
    name: 'sell',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'instrumentId', type: 'bytes32' },
      { name: 'yieldTokenAmount', type: 'uint256' },
      { name: 'minOutputAmount', type: 'uint256' },
    ],
    outputs: [{ name: 'outputAmount', type: 'uint256' }],
  },
] as const;

const walletClient = createWalletClient({
  chain: base,
  transport: custom(window.ethereum),
});

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

Recipe 1: Same-Chain Buy on Base

const account = '0xYourWalletAddress';
const router = '0xbFdd5bEdC0cB9B8795A93C2a1fB634012C8F99bC';
const usdc = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const instrumentId = '0x00002105c053a3e1290845e12a3eea14926472ce7f15da324cdf0700056fc04b';

const amount = parseUnits('1000', 6);
const minDepositedAmount = 0n;

const approvalHash = await walletClient.writeContract({
  account,
  chain: base,
  address: usdc,
  abi: ERC20_ABI,
  functionName: 'approve',
  args: [router, amount],
});

await publicClient.waitForTransactionReceipt({ hash: approvalHash });

const buyHash = await walletClient.writeContract({
  account,
  chain: base,
  address: router,
  abi: ROUTER_ABI,
  functionName: 'buy',
  args: [instrumentId, amount, minDepositedAmount, false, 0n],
});

console.log('buy tx:', buyHash);
For same-asset deposits such as Base Aave USDC, minDepositedAmount = 0n is usually acceptable because there is no internal token conversion.

Recipe 2: Cross-Chain Buy From Base To Arbitrum

When the instrument lives on another supported chain, the same buy() entrypoint routes into the bridge path automatically.
const account = '0xYourWalletAddress';
const router = '0xbFdd5bEdC0cB9B8795A93C2a1fB634012C8F99bC';
const usdc = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// Arbitrum Morpho / Clearstar USDC Reactor instrument
const instrumentId = '0x0000a4b194d4938ed6aab5bdbac7ca4b622f3639b1bca1b8b9c3271403d3b1b5';

const amount = parseUnits('2.20', 6);
const minDepositedAmount = 0n;

const approvalHash = await walletClient.writeContract({
  account,
  chain: base,
  address: usdc,
  abi: ERC20_ABI,
  functionName: 'approve',
  args: [router, amount],
});

await publicClient.waitForTransactionReceipt({ hash: approvalHash });

const sourceTxHash = await walletClient.writeContract({
  account,
  chain: base,
  address: router,
  abi: ROUTER_ABI,
  functionName: 'buy',
  args: [instrumentId, amount, minDepositedAmount, false, 0n],
});

console.log('source tx:', sourceTxHash);

Monitor cross-chain completion

The destination-side redeem is completed asynchronously after attestation. Poll the relay status endpoint using the source tx hash.
async function pollRelay(sourceTxHash: string) {
  while (true) {
    const response = await fetch(
      `https://api.1tx.fi/api/v1/cctp/relay/tx/${sourceTxHash}`,
      {
        headers: {
          'X-API-Key': apiKey,
        },
      },
    );

    if (response.status === 404) {
      await new Promise((resolve) => setTimeout(resolve, 5000));
      continue;
    }

    const relay = await response.json();
    if (relay.status === 'success') {
      console.log('destination tx:', relay.destinationTxHash);
      return relay;
    }

    if (relay.status === 'failed') {
      throw new Error(relay.error || 'relay failed');
    }

    await new Promise((resolve) => setTimeout(resolve, 10000));
  }
}
Direct contract callers do not need to register an operation with the backend. The relay starts from the bridge event webhook.

Recipe 3: Sell Directly Through the Router

const account = '0xYourWalletAddress';
const router = '0xbFdd5bEdC0cB9B8795A93C2a1fB634012C8F99bC';
const yieldToken = '0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB';
const instrumentId = '0x00002105c053a3e1290845e12a3eea14926472ce7f15da324cdf0700056fc04b';

const yieldTokenAmount = parseUnits('500', 18);
const minOutputAmount = 0n;

const approvalHash = await walletClient.writeContract({
  account,
  chain: base,
  address: yieldToken,
  abi: ERC20_ABI,
  functionName: 'approve',
  args: [router, yieldTokenAmount],
});

await publicClient.waitForTransactionReceipt({ hash: approvalHash });

const sellHash = await walletClient.writeContract({
  account,
  chain: base,
  address: router,
  abi: ROUTER_ABI,
  functionName: 'sell',
  args: [instrumentId, yieldTokenAmount, minOutputAmount],
});

console.log('sell tx:', sellHash);

When To Use Direct Contracts vs The Transaction API

Use direct contract calls when you want:
  • fully custom wallet UX,
  • explicit control over approvals and router params,
  • to avoid backend execution helpers for calldata generation.
Use the Transactions API when you want:
  • automatic source-chain selection,
  • quote normalization and slippage handling,
  • ready-to-sign transaction bundles,
  • less integration logic in your client.

Common Pitfalls

  • forgetting that amount for USDC uses 6 decimals
  • not waiting for approval confirmation before sending buy() or sell()
  • assuming cross-chain buys finish in the same transaction as the source-chain buy()
  • hardcoding outdated addresses instead of checking the deployment docs
  • using the wrong chain for the router call when the instrument lives elsewhere

Next Steps

Contract Addresses

Router, bridge, receiver, and token addresses on each supported chain

API Cookbook

API-assisted execution flow with bundle building and relay monitoring