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.
USD values — GatewayTokenAmountV2 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 getOrders — getOrders 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 status — success 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.
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.
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.
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.
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})`); } }});
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.
For offramp (BOB → Bitcoin) orders, getOrders surfaces V2 status fields you can act on while the order is still in progress:
Track the Pending BTC Payout
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.
Refund Stuck Orders
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 availableconst 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.
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.
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%.
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.
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.
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.