Skip to main content

Overview

The BOB Gateway SDK makes it easy to bring Bitcoin onramp functionality directly into your app. This guide walks through the complete integration process. We recommend using the API directly, but you may use our SDK for convenience.

What’s new in V2

This guide targets the V2 API. V1 endpoints remain reachable but are deprecated.
V2 adds:
  • Multi-affiliate fees — split fees across multiple recipients per quote (details).
  • tokenSwap route — EVM-to-EVM token swaps, replacing the V1 layerZero variant.
  • USD valuesGatewayTokenAmountV2 carries an optional usd field on every token amount and on each line of the V2 fee breakdowns.
  • Price impact — V2 quotes expose priceImpact (fraction) and priceImpactUsd for display.
  • Paginated getOrdersgetOrders now accepts { userAddress, cursor?, limit? } and returns { orders, nextCursor } instead of a bare array. Pass back nextCursor to fetch the next page; a null/missing value means you’ve reached the end (details).
  • Order settlement details in statussuccess and refunded are now objects carrying receivedTokens / refundedTokens (each entry has chain, token, amount, and the settlement txHash). In V1 these statuses were bare strings, and the destination txHash lived on dstInfo; in V2 dstInfo no longer carries a txHash — read it from the status payload instead.
  • pendingBtcPayment on offramp orders — while an offramp order is in progress, status.inProgress.pendingBtcPayment exposes the gateway’s outgoing Bitcoin { txid, amount } so you can show the user the pending payout. This replaces the V1 bumpFeeTx field, which is no longer returned.

Installation

npm install @gobob/bob-sdk viem

Initialize the SDK

Import the GatewayApiClient (exported as GatewaySDK) and create an instance. The constructor takes an optional options object:
import { GatewaySDK, STAGING_GATEWAY_BASE_URL } from '@gobob/bob-sdk';

// Mainnet (default)
const gatewaySDK = new GatewaySDK();

// Staging
const gatewaySDKStaging = new GatewaySDK({ basePath: STAGING_GATEWAY_BASE_URL });

Authentication

Gateway API keys are optional. All V2 endpoints are reachable without authentication; an API key unlocks higher rate limits and access to partner features. To request a key, contact the BOB team. Once you have one, pass it in the options object:
import { GatewaySDK } from '@gobob/bob-sdk';

const gatewaySDK = new GatewaySDK({ apiKey: 'your-api-key' });
The API key must be exactly 32 characters long. When provided, the SDK will include it in the Authorization header as a Bearer token (Authorization: Bearer <api-key>). If you’re calling the API directly, set the same header on every V2 request.

Get Available Routes

Fetch all supported routes to show users their options:
const routes = await gatewaySDK.getRoutes();

// Routes include information about:
// - Source and destination chains
// - Supported tokens
// - Available bridges
// - Fee structures

Get a Quote

Request a quote for the user’s desired transaction:
import { parseBtc } from '@gobob/bob-sdk';
import { parseEther } from 'viem';

const quote = await gatewaySDK.getQuote({
  fromChain: 'bitcoin',
  fromToken: '0x0000000000000000000000000000000000000000',
  fromUserAddress: 'bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d',
  toChain: 'bob',
  toToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
  toUserAddress: '0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c',
  amount: parseBtc("0.1"), // 0.1 BTC
  gasRefill: parseEther("0.00001"), // Optional ETH gas refill
});
Token parameters (fromToken, toToken) must be 0x-prefixed hex addresses, not symbols. Use getRoutes() to find supported token addresses. For BTC, use the zero address 0x0000000000000000000000000000000000000000.
Display quote fields like fees and estimated time to give users transparency about the transaction. See the section below for how to access these fields.

Understanding Quote Types

The getQuote response is a discriminated union — access fields through the appropriate key:
const quote = await gatewaySDK.getQuote({ /* ... */ });

if ('onramp' in quote) {
  // Bitcoin → BOB/EVM onramp
  console.log('Input:', quote.onramp.inputAmount);          // GatewayTokenAmountV2 (has optional .usd)
  console.log('Fees:', quote.onramp.feeBreakdown);          // each line is GatewayTokenAmountV2
  console.log('Price impact:', quote.onramp.priceImpact);   // optional, fraction e.g. "-0.05"
  console.log('ETA:', quote.onramp.estimatedTimeInSecs, 'seconds');
} else if ('offramp' in quote) {
  // BOB/EVM → Bitcoin offramp
  console.log('Input:', quote.offramp.inputAmount);
  console.log('Fees:', quote.offramp.feeBreakdown);
  console.log('Price impact:', quote.offramp.priceImpact);
  console.log('ETA:', quote.offramp.estimatedTimeInSecs, 'seconds');
} else if ('tokenSwap' in quote) {
  // EVM token swap (V2)
  console.log('Input:', quote.tokenSwap.inputAmount);
  console.log('Fees:', quote.tokenSwap.fees);
  console.log('Price impact:', quote.tokenSwap.priceImpact);
  console.log('ETA:', quote.tokenSwap.estimatedTimeInSecs, 'seconds');
}
You don’t need to handle all quote types — the response type matches your fromChain/toChain parameters. fromChain: 'bitcoin' with toChain: 'bob' always returns an onramp quote; an EVM-to-EVM pair returns a tokenSwap quote.

Execute the Quote

Execute the quote by having the user sign the Bitcoin transaction:
import { createPublicClient, createWalletClient, http, zeroAddress } from 'viem';
import { useAppKitProvider, useAppKitAccount } from '@reown/appkit/react';
import type { BitcoinConnector } from "@reown/appkit-adapter-bitcoin";
import { ReownWalletAdapter } from '@gobob/bob-sdk';
import { bob } from 'viem/chains';

// Setup viem clients
const publicClient = createPublicClient({
  chain: bob,
  transport: http(),
});

const walletClient = createWalletClient({
  chain: bob,
  transport: http(),
  account: zeroAddress, // Replace with connected account
});

// Get Bitcoin wallet provider
const { walletProvider } = useAppKitProvider<BitcoinConnector>('bip122');
const { address: btcAddress } = useAppKitAccount();

// Execute the quote
const txId = await gatewaySDK.executeQuote({
  quote,
  walletClient,
  publicClient,
  btcSigner: new ReownWalletAdapter(walletProvider, btcAddress),
});

console.log('Transaction ID:', txId);
For detailed wallet integration options including Reown AppKit, sats-wagmi, Dynamic.xyz, and more, see the Bitcoin Wallets guide.

Monitor Orders

Fetch a page of the user’s pending and completed orders. getOrders returns { orders, nextCursor } — pass nextCursor back to walk subsequent pages:
const { orders, nextCursor } = await gatewaySDK.getOrders({
  userAddress: userEvmAddress,
  limit: 20, // optional; omit to use the gateway default
});

orders.forEach(order => {
  console.log(`Source: ${order.srcInfo.amount} ${order.srcInfo.token} (${order.srcInfo.chain})`);
  console.log(`Destination (estimated): ${order.dstInfo.amount} ${order.dstInfo.token} (${order.dstInfo.chain})`);

  // V2 status is always a discriminated object — no bare strings
  if ('inProgress' in order.status) {
    console.log('Status: in progress');
    if (order.status.inProgress.pendingBtcPayment) {
      const { txid, amount } = order.status.inProgress.pendingBtcPayment;
      console.log(`Pending BTC payout: ${amount} sats (txid: ${txid})`);
    }
    if (order.status.inProgress.refundTx) {
      console.log('Refund transaction available');
    }
  } else if ('failed' in order.status) {
    console.log('Status: failed');
    if (order.status.failed.refundTx) {
      console.log('Refund transaction available');
    }
  } else if ('success' in order.status) {
    console.log('Status: success');
    // V2: settled token transfers (with on-chain txHash) are on the status payload
    for (const t of order.status.success.receivedTokens) {
      console.log(`Received ${t.amount} ${t.token} on ${t.chain} (tx ${t.txHash})`);
    }
  } else if ('refunded' in order.status) {
    console.log('Status: refunded');
    for (const t of order.status.refunded.refundedTokens) {
      console.log(`Refunded ${t.amount} ${t.token} on ${t.chain} (tx ${t.txHash})`);
    }
  }
});

Paginating through all orders

nextCursor is null/absent once you’ve reached the last page:
let cursor: string | undefined;
do {
  const page = await gatewaySDK.getOrders({
    userAddress: userEvmAddress,
    limit: 50,
    cursor,
  });
  // ...handle page.orders
  cursor = page.nextCursor ?? undefined;
} while (cursor);
order.dstInfo.amount is the estimated output recorded when the order was created. The settled amount and destination txHash are reported on status.success.receivedTokens (or status.refunded.refundedTokens) once the order resolves.

Offramp Features

For offramp (BOB → Bitcoin) orders, getOrders surfaces V2 status fields you can act on while the order is still in progress:
While the gateway is settling an offramp, status.inProgress.pendingBtcPayment carries the outgoing Bitcoin transaction { txid, amount }. Use it to show the user a “payout in flight” state and link to a block explorer:
const { orders } = await gatewaySDK.getOrders({ userAddress: userEvmAddress });

const inFlight = orders.find(order =>
  'inProgress' in order.status && order.status.inProgress.pendingBtcPayment
);

if (inFlight && 'inProgress' in inFlight.status) {
  const { txid, amount } = inFlight.status.inProgress.pendingBtcPayment!;
  console.log(`Gateway is sending ${amount} sats — track it at https://mempool.space/tx/${txid}`);
}
V2 no longer exposes a bumpFeeTx EVM transaction — the gateway manages fee bumps internally for the BTC payout it broadcasts.
If an order gets stuck or needs to be cancelled, the order will include a refundTx to unlock the locked assets:
const { orders } = await gatewaySDK.getOrders({ userAddress: userEvmAddress });

// Find order with refund transaction available
const orderNeedingRefund = orders.find(order =>
  ('inProgress' in order.status && order.status.inProgress.refundTx)
  || ('failed' in order.status && order.status.failed.refundTx)
);

if (orderNeedingRefund) {
  const refundTx = 'failed' in orderNeedingRefund.status
    ? orderNeedingRefund.status.failed.refundTx!
    : (orderNeedingRefund.status as { inProgress: { refundTx: any } }).inProgress.refundTx!;
  // Submit the refund transaction
  const hash = await walletClient.sendTransaction({
    to: refundTx.to,
    data: refundTx.data,
    value: refundTx.value,
  });

  await publicClient.waitForTransactionReceipt({ hash });
}
This action is irreversible. Once refunded, the order cannot be resumed.

Monetization (Affiliate Fees)

Gateway supports affiliate fees out of the box. You can route a basis-point (bps) cut of each swap to one or more EVM recipient addresses, configured per-quote. V2 supports splitting fees across multiple recipients in a single quote.

How it works

Affiliate fees are basis-point cuts of the swap output, deducted at settlement and sent to the recipient addresses you specify. You set them per-quote via the SDK’s affiliates parameter — an array of { address, bps } pairs (the raw API serialises these as the comma-separated affiliates query parameter on GET /v2/get-quote). 1 bps = 0.01%, so 50 means 0.50%.

Single recipient

const quote = await gatewaySDK.getQuote({
  // ... other params
  affiliates: [{ address: '0xYourAddress', bps: 50 }], // 0.50% to one recipient
});

Split fees across multiple recipients

V2 lets you split affiliate fees across multiple recipients in a single quote — useful for revenue splits between an aggregator and an underlying integrator, referral programs, or multi-party agreements.
const quote = await gatewaySDK.getQuote({
  // ... other params
  affiliates: [
    { address: '0xPartnerA', bps: 50 }, // 0.50%
    { address: '0xPartnerB', bps: 25 }, // 0.25%
  ],
});

Format and rules

  • Comma-separated <address>:<bps> pairs, no spaces.
  • Each address must be a valid EVM address.
  • Each bps MUST be greater than 0.
  • Omit affiliates or pass an empty array for no affiliate fees.
  • The gateway enforces caps on recipient count and total bps. Routes that don’t support affiliate fees return error code AFFILIATE_FEES_NOT_SUPPORTED_FOR_ROUTE — handle this by retrying the quote with affiliates omitted, or surfacing the error to the user.

Reading resolved fees from the quote

The V2 quote response includes a resolved affiliates array — each entry has the recipient address and the computed fee amount (with optional USD value). Use it to surface the affiliate split to the user:
const quote = await gatewaySDK.getQuote({ /* ... */ });

const onramp = 'onramp' in quote ? quote.onramp : null;
if (onramp?.affiliates?.length) {
  for (const a of onramp.affiliates) {
    console.log(
      `${a.address} earns ${a.fee.amount} ${a.fee.address}` +
      (a.fee.usd ? ` (~$${a.fee.usd})` : '')
    );
  }
}
The affiliates field is present on onramp and offramp V2 quotes. tokenSwap quotes don’t currently expose a resolved affiliates list.

Raw API equivalent

For integrators not using the SDK, pass the same pairs to the V2 quote endpoint as the affiliates query parameter:
GET /v2/get-quote?...&affiliates=0xPartnerA:50,0xPartnerB:25
URL-encode the comma if your client doesn’t allow raw commas in query strings.

Next Steps

Build DeFi Strategies

Create 1-click DeFi actions like staking and lending

Bitcoin Wallets

Detailed guide on wallet integrations

API Reference

Complete API documentation

Example Code

View complete examples on GitHub