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.
What's new in V3
This guide targets the V3 API. V1 and V2 endpoints remain reachable but are superseded — the SDK calls /v3 under the hood. Upgrading an existing integration? See the Migration Guide.
The V1 API will be deprecated at the end of July 2026 and shut down afterwards. Migrate any V1 integration to V3 before that date — see the Migration Guide. V2 remains supported.
V3 builds on V2 with multi-chain support and richer settlement data:
- Non-EVM chains (Tron & Solana) — quotes and orders now span EVM, Tron, and Solana as well as Bitcoin. Addresses and token identifiers are
0x…on EVM chains and Base58 on Tron/Solana.executeQuotesigns and broadcasts the right transaction shape for each chain; if you build transactions yourself,createOrderreturns a tagged union you dispatch on (type: 'evm' | 'tron' | 'solana'). ownerAddress(required) andrefundAddress—getQuotenow takes anownerAddress(the EVM owner / refund-claimant, also used for order lookup) and an optional BitcoinrefundAddressfor onramps. Routes that need an owner but don't get one fail withMISSING_OWNER_ADDRESS.- USD on order info —
srcInfo,dstInfo, settled transfers (receivedTokens/refundedTokens), andpendingBtcPaymentnow each carry an optionalusdvalue (valued at order time). In V2,usdwas only on quote amounts and fee-breakdown lines. - Affiliate fees on
tokenSwap—tokenSwapquotes now expose a resolved singleaffiliate({ address, bps }), charged by the aggregator on the source chain. Aggregators allow only one partner fee per swap, so passing more than one affiliate returnsTOO_MANY_AFFILIATES.onramp/offrampkeep the V2 multi-recipientaffiliateslist (details). - Removed parameters —
gasRefill,strategyAddress/strategyTarget, andstrategyMessageare no longer supported and are rejected by the gateway. - Paginated
getOrders—getOrdersaccepts{ userAddress, cursor?, limit? }and returns{ orders, nextCursor }(carried over from V2). Pass backnextCursorto fetch the next page; a null/missing value means you've reached the end (details). - Discriminated order status —
success/refundedare objects carryingreceivedTokens/refundedTokens(each entry haschain,token,amount, the settlementtxHash, and nowusd);status.inProgress.pendingBtcPaymentexposes the gateway's outgoing Bitcoin{ txid, amount, usd }on in-progress X-to-BTC orders. Read the destinationtxHashfrom the status payload —dstInfodoesn't carry one.
Installation
npm install @gobob/bob-sdk viemInitialize 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 V3 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 structuresGet a Quote
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',
toChain: 'bob',
toToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
toUserAddress: '0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c',
ownerAddress: '0x2D2E86236a5bC1c8a5e5499C517E17Fb88Dbc18c', // EVM owner / refund-claimant
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.
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 (V3)
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');
// V3: 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:
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 => {
// V3: srcInfo/dstInfo now 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})`);
}
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');
// Settled token transfers (with on-chain txHash, and V3 `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})`);
}
}
});Link users to the Gateway Explorer
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:
While the gateway is settling an X-to-BTC order, 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}`);
}Gateway no longer exposes a bumpFeeTx EVM transaction — it 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 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 atokenSwapquote accepts exactly one affiliate. Passing more than one returnsTOO_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
bpsMUST be greater than0. - Omit
affiliatesor pass an empty array for no affiliate fees. tokenSwapaccepts at most one affiliate; more than one returnsTOO_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 withaffiliatesomitted, 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 V3 quote endpoint as the affiliates query parameter:
GET /v3/get-quote?...&affiliates=0xPartnerA:50,0xPartnerB:25URL-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.