BOB Gateway

Gateway SDK

Step-by-step guide to integrate BOB Gateway SDK into your application

Overview

The BOB Gateway SDK makes it easy to bring native BTC swaps 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.

V4 integration

The SDK calls /v4 under the hood. See the Migration Guide for the API and generated-type changes.

  • Pass a refundAddress on the source chain for every quote you intend to execute. It is optional for a price-only quote, but order creation rejects a quote without one with MISSING_REFUND_ADDRESS. The SDK forwards the address you provide; it does not infer it from fromUserAddress.
  • Remove ownerAddress from quote parameters. It is no longer part of the V4 request.
  • Bitcoin signers must return signed raw transaction hex. The SDK submits it to Gateway for validation and broadcasting. Offramp and token-swap transactions are indexed from the source chain without register-tx.
  • executeQuote supports Bitcoin signing and viem-compatible EVM/Tron clients. Solana source transactions require the API and a Solana signer; the SDK does not sign Solana VersionedTransaction payloads. Solana can still be a destination for supported routes.

Quotes retain affiliate fees, optional USD amounts, and price impact. Orders retain pagination and discriminated settlement status. Use getRoutes() to discover current chains and tokens.

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

API keys are optional — the API is reachable without authentication. A key unlocks:

  • Analytics dashboard — your orders, volume, and affiliate earnings
  • Higher rate limits than keyless usage

Partner Onboarding

Get an API key and go live — full flow and contact details

Once you have a key, 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 V4 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

Omit maxSlippage to let Gateway recommend a slippage tolerance for the route. The SDK leaves the API's slippage parameter unset and returns the resolved value in the quote. To set your own tolerance, supply basis points, for example maxSlippage: 300 for 3%. The SDK no longer applies a fixed 3% default; see the migration notes and recommendation example. Always review the quote's resolved slippage before execution.

Request a quote for the user's desired transaction:

import { parseBtc } from '@gobob/bob-sdk';

const quote = await gatewaySDK.getQuote({
  fromChain: 'bitcoin',
  fromToken: '0x0000000000000000000000000000000000000000',
  fromUserAddress: 'bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d',
  refundAddress: 'bc1qafk4yhqvj4wep57m62dgrmutldusqde8adh20d', // Bitcoin source-chain refund
  toChain: 'bob',
  toToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
  toUserAddress: '0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c',
  amount: parseBtc("0.1"), // 0.1 BTC
});

On EVM chains, token parameters (fromToken, toToken) must be 0x-prefixed hex addresses, not symbols; on Tron/Solana they are Base58 identifiers. 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.

Request a Slippage Recommendation

Omit maxSlippage to request a quote with Gateway's recommended tolerance. You can inspect the recommendation without creating an order:

import { GatewaySDK, getInnerQuote, parseBtc } from '@gobob/bob-sdk';

const gateway = new GatewaySDK();
const quote = await gateway.getQuote({
  fromChain: 'bitcoin',
  fromToken: '0x0000000000000000000000000000000000000000',
  toChain: 'bob',
  toToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
  toUserAddress: '0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c',
  amount: parseBtc('0.1'),
  // Omit maxSlippage to request Gateway's recommendation.
});

const slippageBps = Number(getInnerQuote(quote).slippage);
console.log(`Recommended slippage: ${slippageBps} bps (${slippageBps / 100}%)`);

The equivalent API request leaves out the slippage query parameter entirely:

curl --get 'https://gateway-api-mainnet.gobob.xyz/v4/get-quote' \
  --data-urlencode 'srcChain=bitcoin' \
  --data-urlencode 'dstChain=bob' \
  --data-urlencode 'srcToken=0x0000000000000000000000000000000000000000' \
  --data-urlencode 'dstToken=0x0555E30da8f98308EdB960aa94C0Db47230d2B9c' \
  --data-urlencode 'recipient=0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c' \
  --data-urlencode 'amount=10000000'

Read onramp.slippage, offramp.slippage, or tokenSwap.slippage in the response. The onramp value is a string and the other variants use numbers; all are in basis points.

Show the resolved tolerance to the user before execution. These examples request a quote only: include a source-chain refundAddress in a fresh quote before creating an order. If the user accepts the tolerance, pass that returned quote unchanged to create-order. If they want a different limit, request a new quote with an explicit slippage / maxSlippage; do not edit the returned quote.

V3 still requires slippage. Older SDK versions send a fixed 3% when maxSlippage is omitted; upgrade to use this behavior, or call the V4 API directly. See the migration notes.

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) {
  // BTC to BOB/EVM
  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) {
  // EVM to BTC
  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) {
  // Cross-chain token swap
  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');
  // The resolved single affiliate (null for fee-free swaps)
  if (quote.tokenSwap.affiliate) {
    const { address, bps } = quote.tokenSwap.affiliate;
    console.log(`Affiliate: ${bps} bps to ${address}`);
  }
}

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; a token-to-token pair (including to Tron/Solana) returns a tokenSwap quote.

Execute the Quote

Execute the quote by having the user sign the Bitcoin transaction. executeQuote returns { order, tx }; keep the order ID to monitor settlement. Without btcSigner, it returns an onramp order with no tx so the user can pay its Bitcoin deposit address externally. A source-chain refundAddress is still required.

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 { order, tx } = await gatewaySDK.executeQuote({
  quote,
  walletClient,
  publicClient,
  btcSigner: new ReownWalletAdapter(walletProvider, btcAddress),
});

console.log('Order:', order, 'Transaction ID:', tx);

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 => {
  // srcInfo/dstInfo carry an optional `usd` value alongside the amount
  console.log(`Source: ${order.srcInfo.amount} ${order.srcInfo.token} (${order.srcInfo.chain})${order.srcInfo.usd ? ` ~$${order.srcInfo.usd}` : ''}`);
  console.log(`Destination (estimated): ${order.dstInfo.amount} ${order.dstInfo.token} (${order.dstInfo.chain})`);

  // Order 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})`);
    }
    // Note: `refundTx` is always null — Gateway settles refunds itself
  } else if ('failed' in order.status) {
    console.log('Status: failed');
  } else if ('success' in order.status) {
    console.log('Status: success');
    // Settled token transfers (with on-chain txHash, and optional `usd`) 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})${t.usd ? ` ~$${t.usd}` : ''}`);
    }
  } 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})`);
    }
  }
});

If you don't want to build your own tracking UI, every order has a public status page on the Gateway Explorer that you can link users to directly:

https://gateway-explorer.gobob.xyz/order/<ORDER_ID>

Example: gateway-explorer.gobob.xyz/order/71a070e7-...

Use the order ID returned when the order is created. The page shows live status, amounts, and transaction hashes for both sides of the swap — useful as a "track your swap" link in confirmation screens, order history, or notification emails.

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.

X to BTC Order Features

For X-to-BTC (BOB → Bitcoin) orders, getOrders surfaces status fields you can act on while the order is still in progress:

Monetization (Affiliate Fees)

Gateway supports affiliate fees out of the box. You set them per-quote via the SDK's affiliates parameter — an array of { address, bps } pairs. How they're charged depends on the route:

  • onramp / offramp — fees are deducted at settlement and paid out in USDT on Ethereum to the recipient addresses you specify, regardless of the route. These routes accept multiple recipients per quote.
  • tokenSwap — the fee is charged by the aggregator (Bungee/Velora) on the source chain. Aggregators allow only one partner fee per swap, so a tokenSwap quote accepts exactly one affiliate. Passing more than one returns TOO_MANY_AFFILIATES.

1 bps = 0.01%, so 50 means 0.50%. For the full fee model, see Fees.

Single recipient

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

Split fees across multiple recipients

onramp and offramp quotes let 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. (tokenSwap accepts only one affiliate.)

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.
  • tokenSwap accepts at most one affiliate; more than one returns TOO_MANY_AFFILIATES.
  • 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

onramp and offramp quotes include 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})` : '')
    );
  }
}

tokenSwap quotes instead expose a single resolved affiliate ({ address, bps }, or null for a fee-free swap):

const tokenSwap = 'tokenSwap' in quote ? quote.tokenSwap : null;
if (tokenSwap?.affiliate) {
  const { address, bps } = tokenSwap.affiliate;
  console.log(`${address} earns ${bps} bps on the source chain`);
}

Raw API equivalent

For integrators not using the SDK, pass the same pairs to the V4 quote endpoint as the affiliates query parameter:

GET /v4/get-quote?...&affiliates=0xPartnerA:50,0xPartnerB:25

URL-encode the comma if your client doesn't allow raw commas in query strings.

Track your orders and earnings

With an API key you get a partner dashboard on the Gateway Explorer:

https://gateway-explorer.gobob.xyz/affiliate/<API_KEY>

It shows your integration's orders, volume, and — if you charge affiliate fees — your accumulated earnings. For ecosystem-wide activity, see the public Dune dashboard.

Next Steps

On this page