Cross-Chain Transfer Guide
This guide covers integrating the Zet Cross-Chain Transfer API to move tokens between different blockchains.
Overview
Cross-chain transfers use the same underlying infrastructure as cross-chain swaps (LiFi aggregation) but are designed specifically for moving tokens from one chain to another, optionally converting to a different token on the destination chain.
Your App Zet API Bridge Protocol
│ │ │
├── GET /transfer/chains ──►│ │
│◄── Chain + token list ────┤ │
│ │ │
├── POST /transfer/quote ──►│── Find route ───────────►│
│◄── Quote + bridge info ───┤◄── Best bridge ──────────┤
│ │ │
├── POST /transfer/execute ►│ │
│ │── Approve + bridge ──► Source Chain
│ │ Bridge relay ──► Dest Chain
│ │── Poll status ──────────►│
│ │ │
│◄── Webhook: transfer.completed ◄─────────────────────┤
│ │ │
Supported chains
Source chains (send from)
| Chain | Chain ID | Tokens |
|---|
| Base | 8453 | ETH, USDC, CNGN, cbBTC |
| BSC | 56 | BNB, USDC, USDT, CNGN |
Destination chains (send to)
| Chain | Chain ID |
|---|
| Base | 8453 |
| BSC | 56 |
| Ethereum | 1 |
| Polygon | 137 |
| Arbitrum | 42161 |
| Avalanche | 43114 |
| Solana | 1151111081099710 |
You can also query supported chains dynamically via GET /transfer/chains.
Step 1: Get a transfer quote
Transfer 100 USDC from Base to Ethereum:
const quote = await fetch('https://api.zet.money/v1/transfer/quote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ZET_API_KEY,
},
body: JSON.stringify({
fromChain: 'base',
toChain: 'ethereum',
token: 'USDC',
toToken: 'USDC', // Same token on destination
amount: '100',
destinationAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f5bA16',
}),
}).then(r => r.json());
Bridge + swap
Transfer USDC from Base and receive ETH on Ethereum:
const quote = await fetch('https://api.zet.money/v1/transfer/quote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ZET_API_KEY,
},
body: JSON.stringify({
fromChain: 'base',
toChain: 'ethereum',
token: 'USDC',
toToken: 'ETH', // Different token on destination
amount: '100',
destinationAddress: '0x742d35Cc...',
}),
}).then(r => r.json());
Quote response
{
"success": true,
"data": {
"quoteId": "qt_01H8X6abc",
"fromChain": "base",
"toChain": "ethereum",
"fromToken": "USDC",
"toToken": "USDC",
"fromAmount": "100",
"toAmount": "99.85",
"fees": {
"platformFee": "0.032",
"platformFeeFiat": "50",
"bridgeFee": "0.15",
"totalFee": "0.182"
},
"route": {
"steps": 2,
"estimatedTime": "2-5 minutes",
"bridge": "Stargate"
},
"expiresAt": "2026-03-02T10:05:00Z"
}
}
Fee breakdown
| Fee | Amount | Description |
|---|
| Platform fee | 50 NGN (in source token) | Zet cross-chain fee |
| Bridge fee | Variable | Set by the bridge protocol (Stargate, Across, etc.) |
Step 2: Execute the transfer
Custodial
const transfer = await fetch('https://api.zet.money/v1/transfer/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ZET_API_KEY,
},
body: JSON.stringify({
quoteId: quote.data.quoteId,
walletId: 'wal_01H8X3abc',
reference: `transfer_${transferId}`,
}),
}).then(r => r.json());
Non-custodial
const transfer = await fetch('https://api.zet.money/v1/transfer/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ZET_API_KEY,
},
body: JSON.stringify({
quoteId: quote.data.quoteId,
sourceAddress: '0x742d35Cc...',
reference: `transfer_${transferId}`,
}),
}).then(r => r.json());
// User signs approval + bridge transaction
if (transfer.data.approvalData) {
await userWallet.sendTransaction(transfer.data.approvalData);
}
await userWallet.sendTransaction(transfer.data.transactionData);
Step 3: Monitor completion
Cross-chain transfers take longer than same-chain operations because they involve bridge relay confirmation on the destination chain.
| Bridge | Estimated Time |
|---|
| Stargate | 1-5 minutes |
| Across | 2-10 minutes |
| Hop | 5-20 minutes |
Listen for the transfer.completed webhook:
app.post('/webhooks/zet', (req, res) => {
const { event, data } = req.body;
if (event === 'transfer.completed') {
console.log(`Transfer complete on ${data.chain}`);
console.log(`TX: ${data.transactionHash}`);
}
if (event === 'transfer.failed') {
// Funds typically remain on the source chain
console.log(`Transfer failed: ${data.errorMessage}`);
}
res.status(200).send('OK');
});
Common patterns
Portfolio rebalancing
Move tokens between chains to rebalance a user’s portfolio:
// Move 50 USDC from Base to BSC
const quote = await getTransferQuote({
fromChain: 'base',
toChain: 'bsc',
token: 'USDC',
toToken: 'USDC',
amount: '50',
destinationAddress: walletAddress, // Same address, different chain
});
Multi-chain withdrawals
Let users withdraw to any supported chain:
// User wants to withdraw 100 USDC to Polygon
const quote = await getTransferQuote({
fromChain: 'base',
toChain: 'polygon',
token: 'USDC',
toToken: 'USDC',
amount: '100',
destinationAddress: userPolygonAddress,
});
Best practices
- Show estimated time — cross-chain transfers are slower than same-chain; manage user expectations
- Display the bridge protocol — users benefit from knowing which bridge is being used
- Handle stuck transactions — bridges can occasionally slow down; implement timeout alerts
- Prefer same-chain when possible — if both source and destination are on the same chain, use the Swap API instead
- Verify destination address format — ensure the address is valid for the destination chain (especially for Solana)
- Show fee breakdown — bridge fees vary significantly; transparency builds trust