toChain set to a different chain: DZap finds a route and builds the transaction on the source chain, and you sign it, send it, and track it until the tokens arrive. Pick EVM, Solana, or Bitcoin in any tab, and the rest of the page switches with it.
Setup
- EVM
- Solana
- Bitcoin
This example bridges 100 USDC from Arbitrum to your own address on Base, signing with
walletClient, a viem wallet client built from the private key in PRIVATE_KEY. Amounts are strings in the token’s smallest unit, and no API key is needed.import { DZapClient, Services, ApprovalModes, TxnStatus, type TradeStatusResponse } from '@dzapio/sdk';
import { createWalletClient, http } from 'viem';
import { arbitrum } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrum, transport: http() });
const dzap = DZapClient.getInstance();
const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const AMOUNT = '100000000'; // 100 USDC (6 decimals)
This example bridges 100 USDC from Solana to your wallet on Base, set in
recipient. It signs with the keypair in SOLANA_SECRET_KEY using @solana/web3.js, which you install alongside the SDK. Amounts are strings in the token’s smallest unit, and no API key is needed.import { DZapClient, TxnStatus, type TradeStatusResponse } from '@dzapio/sdk';
import { Keypair, VersionedTransaction } from '@solana/web3.js';
const dzap = DZapClient.getInstance();
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.SOLANA_SECRET_KEY!)));
const account = keypair.publicKey.toBase58() as `0x${string}`; // SDK types expect hex; base58 works
const recipient = '0xYourBaseAddress'; // your wallet on Base
const USDC_SOL = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const AMOUNT = '100000000'; // 100 USDC (6 decimals)
This example bridges 0.001 BTC to USDC on Base. Set
account and publicKey to your Bitcoin address and its public key, and recipient to your wallet on Base; your Bitcoin wallet signs the transaction. Amounts are strings in satoshis, and no API key is needed.import { DZapClient, TxnStatus, type TradeStatusResponse } from '@dzapio/sdk';
const dzap = DZapClient.getInstance();
const account = 'bc1q...' as `0x${string}`; // your Bitcoin address; SDK types expect hex
const publicKey = '02...'; // compressed pubkey for `account`
const recipient = '0xYourBaseAddress'; // your wallet on Base
const BTC = '0x0000000000000000000000000000000000000000'; // native BTC
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const AMOUNT = '100000'; // 0.001 BTC (satoshis)
Steps
- EVM
- Solana
- Bitcoin
1
Quote
getTradeQuotes asks the bridge providers DZap supports for a price; a toChain different from fromChain makes it a bridge. pair.quoteRates lists every route, recommendedSource names DZap’s pick, and each route’s duration is its estimated time in seconds. slippage: 1 accepts up to 1% less than quoted.const quotes = await dzap.getTradeQuotes({
fromChain: 42161,
account: account.address,
data: [{
srcToken: USDC_ARB,
destToken: USDC_BASE,
amount: AMOUNT,
toChain: 8453, // Base, different chain = bridge
slippage: 1, // 1 = 1%
}],
});
const [pair] = Object.values(quotes);
const route = pair.quoteRates?.[pair.recommendedSource];
console.log(route?.destAmount, route?.duration); // USDC out on Base, estimated seconds
2
Approve
The DZap router on Arbitrum needs permission to move your USDC.
getAllowance reads the current allowance, and if it’s too low, approve() approves exactly the bridge amount. Nothing needs approving on Base.const { data } = await dzap.getAllowance({
chainId: 42161,
sender: account.address,
tokens: [{ address: USDC_ARB, amount: AMOUNT }],
service: Services.trade,
mode: ApprovalModes.Default,
});
if (data[USDC_ARB].allowance < BigInt(AMOUNT)) {
await dzap.approve({
chainId: 42161,
signer: walletClient,
tokens: [{ address: USDC_ARB, amount: AMOUNT }],
service: Services.trade,
mode: ApprovalModes.Default,
});
}
Other approval flows (Permit2, EIP-2612, gasless permits) are in Check allowance and Approval mechanisms.
3
Build & send
trade() builds the transaction for the recommended route, simulates it, then signs and sends it on Arbitrum. If the simulation fails, nothing is sent and result says why. recipient receives the USDC on Base, and refundee gets it back on Arbitrum if the bridge refunds.const request = {
fromChain: 42161,
sender: account.address,
refundee: account.address,
gasless: false,
data: [{
srcToken: USDC_ARB,
destToken: USDC_BASE,
amount: AMOUNT,
toChain: 8453,
protocol: pair.recommendedSource,
recipient: account.address,
slippage: 1,
}],
};
const result = await dzap.trade({ request, signer: walletClient });
if (result.status !== TxnStatus.success) throw new Error(result.errorMsg);
4
Status
The USDC reaches Base only after the bridge finishes, so poll
getTradeTxnStatus with the Arbitrum transaction hash until the status leaves PENDING; COMPLETED means it arrived. DZap answers 404 until it sees the transaction, and 429 once the keyless limit of 5 requests a minute is used up, so the catch skips only those two and rethrows anything else. The loop gives up after 30 minutes, and the check after it throws unless the status is COMPLETED.let status: TradeStatusResponse | undefined;
for (let i = 0; i < 120; i++) { // gives up after 30 minutes
await new Promise((r) => setTimeout(r, 15_000));
try {
status = await dzap.getTradeTxnStatus({ txHash: result.txnHash!, chainId: 42161 });
} catch (e: any) {
if (![404, 429].includes(e.response?.status)) throw e;
}
if (status && status.status !== 'PENDING') break;
}
if (status?.status !== 'COMPLETED') throw new Error(`Not completed: ${status?.status ?? 'no status'}`);
console.log(status.status); // 'COMPLETED'
1
Quote
getTradeQuotes asks the bridge providers DZap supports for a price from Solana (chain ID 7565164 in DZap) to Base. pair.quoteRates lists every route, recommendedSource names DZap’s pick, and each route’s duration is its estimated time in seconds. slippage: 1 accepts up to 1% less than quoted.const quotes = await dzap.getTradeQuotes({
fromChain: 7565164,
account,
data: [{
srcToken: USDC_SOL,
destToken: USDC_BASE,
amount: AMOUNT,
toChain: 8453, // Base, different chain = bridge
slippage: 1, // 1 = 1%
}],
});
const [pair] = Object.values(quotes);
const route = pair.quoteRates?.[pair.recommendedSource];
console.log(route?.destAmount, route?.duration); // USDC out on Base, estimated seconds
2
Build
No approval on Solana.
buildTradeTxn builds the transaction for the recommended route and simulates it, throwing if it would fail. It returns the unsigned transaction, base64-encoded in data, and a txId for sending it. recipient receives the USDC on Base, and refundee gets it back on Solana if the bridge refunds.const built = await dzap.buildTradeTxn({
fromChain: 7565164,
sender: account,
refundee: account,
gasless: false,
data: [{
srcToken: USDC_SOL,
destToken: USDC_BASE,
amount: AMOUNT,
toChain: 8453,
protocol: pair.recommendedSource,
recipient,
slippage: 1,
}],
});
3
Sign & send
Sign the transaction with your keypair and hand it to
broadcastTradeTx, which submits it and returns its txnHash. Do it right away: the transaction expires about a minute after it’s built.const tx = VersionedTransaction.deserialize(Buffer.from(built.data, 'base64'));
tx.sign([keypair]);
const result = await dzap.broadcastTradeTx({
txId: built.txId,
chainId: 7565164,
txData: Buffer.from(tx.serialize()).toString('base64'),
});
if (result.status !== TxnStatus.success) throw new Error(result.message);
4
Status
The USDC reaches Base only after the bridge finishes, so poll
getTradeTxnStatus with the txnHash until the status leaves PENDING; COMPLETED means it arrived. DZap answers 404 until it sees the transaction, and 429 once the keyless limit of 5 requests a minute is used up, so the catch skips only those two and rethrows anything else. The loop gives up after 30 minutes, and the check after it throws unless the status is COMPLETED.let status: TradeStatusResponse | undefined;
for (let i = 0; i < 120; i++) { // gives up after 30 minutes
await new Promise((r) => setTimeout(r, 15_000));
try {
status = await dzap.getTradeTxnStatus({ txHash: result.txnHash, chainId: 7565164 });
} catch (e: any) {
if (![404, 429].includes(e.response?.status)) throw e;
}
if (status && status.status !== 'PENDING') break;
}
if (status?.status !== 'COMPLETED') throw new Error(`Not completed: ${status?.status ?? 'no status'}`);
console.log(status.status); // 'COMPLETED'
1
Quote
getTradeQuotes asks the bridge providers DZap supports for a price from Bitcoin (chain ID 1000 in DZap) to Base. pair.quoteRates lists every route, recommendedSource names DZap’s pick, and each route’s duration is its estimated time in seconds; routes from Bitcoin take 10 minutes or more. slippage: 1 accepts up to 1% less than quoted.const quotes = await dzap.getTradeQuotes({
fromChain: 1000,
account,
data: [{
srcToken: BTC,
destToken: USDC_BASE,
amount: AMOUNT,
toChain: 8453, // Base, different chain = bridge
slippage: 1, // 1 = 1%
}],
});
const [pair] = Object.values(quotes);
const route = pair.quoteRates?.[pair.recommendedSource];
console.log(route?.destAmount, route?.duration); // USDC out on Base, estimated seconds
2
Build
No approval on Bitcoin.
buildTradeTxn builds the transaction for the recommended route and requires your publicKey. It returns an unsigned PSBT (partially signed Bitcoin transaction), base64-encoded in data, and a txId for sending it. recipient receives the USDC on Base, and refundee gets the BTC back if the bridge refunds.const built = await dzap.buildTradeTxn({
fromChain: 1000,
sender: account,
refundee: account,
gasless: false,
publicKey, // required on Bitcoin
data: [{
srcToken: BTC,
destToken: USDC_BASE,
amount: AMOUNT,
toChain: 8453,
protocol: pair.recommendedSource,
recipient,
slippage: 1,
}],
});
3
Sign & send
Sign and finalize the PSBT with your Bitcoin wallet, then pass the transaction hex to
broadcastTradeTx, which submits it and returns its txnHash.// sign the base64 PSBT in built.data with your Bitcoin wallet, then finalize it into signedTxHex
const result = await dzap.broadcastTradeTx({
txId: built.txId,
chainId: 1000,
txData: signedTxHex, // your signed Bitcoin transaction, as hex
});
if (result.status !== TxnStatus.success) throw new Error(result.message);
4
Status
The USDC reaches Base only after the bridge finishes, so poll
getTradeTxnStatus with the txnHash until the status leaves PENDING; COMPLETED means it arrived. DZap answers 404 until it sees the transaction, and 429 once the keyless limit of 5 requests a minute is used up, so the catch skips only those two and rethrows anything else. The loop gives up after 30 minutes, and the check after it throws unless the status is COMPLETED.let status: TradeStatusResponse | undefined;
for (let i = 0; i < 120; i++) { // gives up after 30 minutes
await new Promise((r) => setTimeout(r, 15_000));
try {
status = await dzap.getTradeTxnStatus({ txHash: result.txnHash, chainId: 1000 });
} catch (e: any) {
if (![404, 429].includes(e.response?.status)) throw e;
}
if (status && status.status !== 'PENDING') break;
}
if (status?.status !== 'COMPLETED') throw new Error(`Not completed: ${status?.status ?? 'no status'}`);
console.log(status.status); // 'COMPLETED'
End-to-end
- EVM
- Solana
- Bitcoin
import { DZapClient, Services, ApprovalModes, TxnStatus, type TradeStatusResponse } from '@dzapio/sdk';
import { createWalletClient, http } from 'viem';
import { arbitrum } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrum, transport: http() });
const dzap = DZapClient.getInstance();
const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const AMOUNT = '100000000'; // 100 USDC
// 1. Quote
const quotes = await dzap.getTradeQuotes({
fromChain: 42161,
account: account.address,
data: [{ srcToken: USDC_ARB, destToken: USDC_BASE, amount: AMOUNT, toChain: 8453, slippage: 1 }],
});
const [pair] = Object.values(quotes);
// 2. Approve (source chain only)
const { data } = await dzap.getAllowance({
chainId: 42161,
sender: account.address,
tokens: [{ address: USDC_ARB, amount: AMOUNT }],
service: Services.trade,
mode: ApprovalModes.Default,
});
if (data[USDC_ARB].allowance < BigInt(AMOUNT)) {
await dzap.approve({
chainId: 42161,
signer: walletClient,
tokens: [{ address: USDC_ARB, amount: AMOUNT }],
service: Services.trade,
mode: ApprovalModes.Default,
});
}
// 3. Build + send
const request = {
fromChain: 42161,
sender: account.address,
refundee: account.address,
gasless: false,
data: [{ srcToken: USDC_ARB, destToken: USDC_BASE, amount: AMOUNT, toChain: 8453, protocol: pair.recommendedSource, recipient: account.address, slippage: 1 }],
};
const result = await dzap.trade({ request, signer: walletClient });
if (result.status !== TxnStatus.success) throw new Error(result.errorMsg);
// 4. Status
let status: TradeStatusResponse | undefined;
for (let i = 0; i < 120; i++) { // gives up after 30 minutes
await new Promise((r) => setTimeout(r, 15_000));
try {
status = await dzap.getTradeTxnStatus({ txHash: result.txnHash!, chainId: 42161 });
} catch (e: any) {
if (![404, 429].includes(e.response?.status)) throw e;
}
if (status && status.status !== 'PENDING') break;
}
if (status?.status !== 'COMPLETED') throw new Error(`Not completed: ${status?.status ?? 'no status'}`);
console.log('settled:', status.status);
import { DZapClient, TxnStatus, type TradeStatusResponse } from '@dzapio/sdk';
import { Keypair, VersionedTransaction } from '@solana/web3.js';
const dzap = DZapClient.getInstance();
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.SOLANA_SECRET_KEY!)));
const account = keypair.publicKey.toBase58() as `0x${string}`; // SDK types expect hex; base58 works
const recipient = '0xYourBaseAddress'; // your wallet on Base
const USDC_SOL = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const AMOUNT = '100000000'; // 100 USDC
// 1. Quote
const quotes = await dzap.getTradeQuotes({
fromChain: 7565164,
account,
data: [{ srcToken: USDC_SOL, destToken: USDC_BASE, amount: AMOUNT, toChain: 8453, slippage: 1 }],
});
const [pair] = Object.values(quotes);
// 2. Build (no approval on Solana)
const built = await dzap.buildTradeTxn({
fromChain: 7565164,
sender: account,
refundee: account,
gasless: false,
data: [{ srcToken: USDC_SOL, destToken: USDC_BASE, amount: AMOUNT, toChain: 8453, protocol: pair.recommendedSource, recipient, slippage: 1 }],
});
// 3. Sign + broadcast via DZap
const tx = VersionedTransaction.deserialize(Buffer.from(built.data, 'base64'));
tx.sign([keypair]);
const result = await dzap.broadcastTradeTx({
txId: built.txId,
chainId: 7565164,
txData: Buffer.from(tx.serialize()).toString('base64'),
});
if (result.status !== TxnStatus.success) throw new Error(result.message);
// 4. Status
let status: TradeStatusResponse | undefined;
for (let i = 0; i < 120; i++) { // gives up after 30 minutes
await new Promise((r) => setTimeout(r, 15_000));
try {
status = await dzap.getTradeTxnStatus({ txHash: result.txnHash, chainId: 7565164 });
} catch (e: any) {
if (![404, 429].includes(e.response?.status)) throw e;
}
if (status && status.status !== 'PENDING') break;
}
if (status?.status !== 'COMPLETED') throw new Error(`Not completed: ${status?.status ?? 'no status'}`);
console.log('settled:', status.status);
import { DZapClient, TxnStatus, type TradeStatusResponse } from '@dzapio/sdk';
const dzap = DZapClient.getInstance();
const account = 'bc1q...' as `0x${string}`; // your Bitcoin address; SDK types expect hex
const publicKey = '02...'; // compressed pubkey for `account`
const recipient = '0xYourBaseAddress'; // your wallet on Base
const BTC = '0x0000000000000000000000000000000000000000'; // native BTC
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const AMOUNT = '100000'; // 0.001 BTC (satoshis)
// 1. Quote
const quotes = await dzap.getTradeQuotes({
fromChain: 1000,
account,
data: [{ srcToken: BTC, destToken: USDC_BASE, amount: AMOUNT, toChain: 8453, slippage: 1 }],
});
const [pair] = Object.values(quotes);
// 2. Build (no approval on Bitcoin; publicKey is required)
const built = await dzap.buildTradeTxn({
fromChain: 1000,
sender: account,
refundee: account,
gasless: false,
publicKey,
data: [{ srcToken: BTC, destToken: USDC_BASE, amount: AMOUNT, toChain: 8453, protocol: pair.recommendedSource, recipient, slippage: 1 }],
});
// 3. Sign the PSBT in built.data with your Bitcoin wallet, then broadcast the finalized transaction as hex
const result = await dzap.broadcastTradeTx({ txId: built.txId, chainId: 1000, txData: signedTxHex });
if (result.status !== TxnStatus.success) throw new Error(result.message);
// 4. Status
let status: TradeStatusResponse | undefined;
for (let i = 0; i < 120; i++) { // gives up after 30 minutes
await new Promise((r) => setTimeout(r, 15_000));
try {
status = await dzap.getTradeTxnStatus({ txHash: result.txnHash, chainId: 1000 });
} catch (e: any) {
if (![404, 429].includes(e.response?.status)) throw e;
}
if (status && status.status !== 'PENDING') break;
}
if (status?.status !== 'COMPLETED') throw new Error(`Not completed: ${status?.status ?? 'no status'}`);
console.log('settled:', status.status);
API usage
The same flow over REST. Non-EVM chains build a chain-specific transaction you sign locally, then submit via Broadcast.- EVM
- Solana
- Bitcoin
curl -X POST https://api.dzap.io/v1/quotes \
-H "Content-Type: application/json" \
-d '{
"fromChain": 42161,
"data": [{
"amount": "100000000",
"srcToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"destToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"toChain": 8453,
"slippage": 1
}]
}'
curl -X POST https://api.dzap.io/v1/buildTx \
-H "Content-Type: application/json" \
-d '{
"sender": "0xYourWallet",
"refundee": "0xYourWallet",
"fromChain": 42161,
"gasless": false,
"data": [{
"amount": "100000000",
"srcToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"destToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"toChain": 8453,
"protocol": "<recommendedSource from quote>",
"recipient": "0xYourWallet",
"slippage": 1
}]
}'
curl "https://api.dzap.io/v1/status?txHash=0xabc...&chainId=42161"
transaction from the build response with your own wallet. Full reference: Quote, Build Tx, Status.curl -X POST https://api.dzap.io/v1/quotes \
-H "Content-Type: application/json" \
-d '{
"fromChain": 7565164,
"data": [{
"amount": "100000000",
"srcToken": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"destToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"toChain": 8453,
"slippage": 1
}]
}'
curl -X POST https://api.dzap.io/v1/buildTx \
-H "Content-Type: application/json" \
-d '{
"sender": "YourSolanaWallet",
"refundee": "YourSolanaWallet",
"fromChain": 7565164,
"gasless": false,
"data": [{
"amount": "100000000",
"srcToken": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"destToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"toChain": 8453,
"protocol": "<recommendedSource from quote>",
"recipient": "0xYourBaseAddress",
"slippage": 1
}]
}'
# sign the base64 transaction from the build response, then submit it
curl -X POST https://api.dzap.io/v1/broadcast \
-H "Content-Type: application/json" \
-d '{
"txId": "<txId from build>",
"chainId": 7565164,
"txData": "<signed transaction>"
}'
curl "https://api.dzap.io/v1/status?txHash=<signature>&chainId=7565164"
curl -X POST https://api.dzap.io/v1/quotes \
-H "Content-Type: application/json" \
-d '{
"fromChain": 1000,
"data": [{
"amount": "100000",
"srcToken": "0x0000000000000000000000000000000000000000",
"destToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"toChain": 8453,
"slippage": 1
}]
}'
curl -X POST https://api.dzap.io/v1/buildTx \
-H "Content-Type: application/json" \
-d '{
"sender": "bc1q...",
"refundee": "bc1q...",
"fromChain": 1000,
"gasless": false,
"publicKey": "02...",
"data": [{
"amount": "100000",
"srcToken": "0x0000000000000000000000000000000000000000",
"destToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"toChain": 8453,
"protocol": "<recommendedSource from quote>",
"recipient": "0xYourBaseAddress",
"slippage": 1
}]
}'
# sign the base64 PSBT from the build response, then submit the finalized transaction as hex
curl -X POST https://api.dzap.io/v1/broadcast \
-H "Content-Type: application/json" \
-d '{
"txId": "<txId from build>",
"chainId": 1000,
"txData": "<signed transaction hex>"
}'
curl "https://api.dzap.io/v1/status?txHash=<txnHash>&chainId=1000"
publicKey is required on the build. Sign the base64 PSBT in data, then broadcast the finalized transaction as hex. Full reference: Quote, Build Tx, Broadcast, Status.What can go wrong
- No route:
pair.quoteRatesis empty;pair.unavailableRoutessays why each provider declined. - The build fails:
trade()returns an error andbuildTradeTxnthrows. The error’sactionsays what to fix;TRY_ANOTHER_ROUTEmeans building with another route frompair.quoteRates. expiredfrombroadcastTradeTxon Solana: build and sign again right away.Not completed: no status: DZap never returned a status, usually because the transaction failed on-chain. Check the hash in a block explorer.- HTTP 429: without an API key, the trade API allows 5 requests a minute in total, shared by every endpoint, and
retryAftersays how many seconds to wait. The status loop skips 429s and polls again.