Build a Bridge Anchor
By AnchorFactory ·
A bridge (asset movement) anchor transfers tokens between Keeta and external chains. This is the most complex anchor type — it requires monitoring multiple blockchains and handling the failure modes that come with cross-chain operations.
What You'll Build
A bridge between Keeta and an external chain (we'll use a generic EVM chain as the example):
Endpoint | Method | Purpose |
|---|---|---|
| POST | Estimate fees and output for a transfer |
| POST | Start a cross-chain transfer (signed request) |
| GET | Check transfer status |
| POST | Create a persistent deposit address (signed request) |
Prerequisites
Node.js 18+
Two Keeta accounts: one for the anchor, one for fee collection
An RPC endpoint for the external chain (e.g., Base, Ethereum)
Understanding of the FX anchor pattern and wallet signatures
Architecture Overview
External Chain → Keeta (Deposit):
1. User sends tokens to anchor's external-chain address
2. Anchor detects deposit via chain monitoring
3. Anchor sends equivalent tokens to user on Keeta
4. Anchor updates transfer status
Keeta → External Chain (Withdrawal):
1. User sends tokens to anchor's Keeta address
2. Anchor detects deposit via Keeta monitoring
3. Anchor sends equivalent tokens on external chain
4. Anchor updates transfer statusStep 1: Token Mapping
Define which tokens can be bridged and their addresses on each chain:
// src/tokens.js
export const TOKEN_MAP = {
USDC: {
keeta: {
address: 'keeta_amnkge74xitii5dsobstldatv3irmyimujfjotftx7plaaaseam4bntb7wnna',
decimals: 6,
},
evm: {
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
decimals: 6,
},
},
// Add more tokens here
};
export function findToken(symbol) {
return TOKEN_MAP[symbol.toUpperCase()] || null;
}
export function convertAmount(amount, fromDecimals, toDecimals) {
// Handle decimal differences between chains
if (fromDecimals === toDecimals) return amount;
if (fromDecimals > toDecimals) {
return amount / BigInt(10 ** (fromDecimals - toDecimals));
}
return amount * BigInt(10 ** (toDecimals - fromDecimals));
}Step 2: Transfer State Machine
Every transfer goes through a defined lifecycle:
// src/transfers.js
export const STATUS = {
PENDING: 'pending', // transfer initiated, awaiting deposit
DETECTED: 'detected', // deposit seen on source chain
CONFIRMING: 'confirming', // waiting for confirmations
SENDING: 'sending', // sending on destination chain
COMPLETED: 'completed', // done
FAILED: 'failed', // something went wrong
REFUNDED: 'refunded', // deposit returned to sender
};Store transfers in a database with full audit trail:
// In your database
const transferSchema = {
id: 'string', // unique transfer ID
status: 'string', // STATUS enum
direction: 'string', // 'deposit' (ext→keeta) or 'withdrawal' (keeta→ext)
symbol: 'string', // e.g. 'USDC'
amount: 'string', // raw BigInt string
sender: 'string', // source address
recipient: 'string', // destination address
sourceTxHash: 'string?', // transaction hash on source chain
destTxHash: 'string?', // transaction hash on destination chain
fee: 'string', // fee amount (raw)
createdAt: 'number',
updatedAt: 'number',
confirmations: 'number', // current confirmation count
requiredConfirmations: 'number', // how many needed
};Step 3: Simulation Endpoint
Let users estimate fees before committing:
// src/routes/simulate.js
import { findToken, convertAmount } from '../tokens.js';
const BRIDGE_FEE_BPS = 30; // 0.30% fee
export function simulate(req, res) {
const { symbol, amount, direction } = req.body;
const token = findToken(symbol);
if (!token) {
return res.status(400).json({ error: 'Unsupported token' });
}
const amountBig = BigInt(amount);
const fee = amountBig * BigInt(BRIDGE_FEE_BPS) / 10000n;
const amountAfterFee = amountBig - fee;
const fromDecimals = direction === 'deposit' ? token.evm.decimals : token.keeta.decimals;
const toDecimals = direction === 'deposit' ? token.keeta.decimals : token.evm.decimals;
const outputAmount = convertAmount(amountAfterFee, fromDecimals, toDecimals);
res.json({
inputAmount: amount,
outputAmount: outputAmount.toString(),
fee: fee.toString(),
feeBps: BRIDGE_FEE_BPS,
estimatedTime: direction === 'deposit' ? '2-5 minutes' : '10-30 minutes',
symbol,
direction,
});
}Step 4: Initiate Transfer
Start a cross-chain transfer with a signed request:
// src/routes/initiate.js
import { v4 as uuid } from 'uuid';
import { findToken } from '../tokens.js';
export async function initiate(req, res) {
// req.verifiedAccount is set by the auth middleware
const { symbol, amount, direction, recipient } = req.body;
const token = findToken(symbol);
if (!token) {
return res.status(400).json({ error: 'Unsupported token' });
}
const transfer = {
id: uuid(),
status: 'pending',
direction,
symbol,
amount,
sender: req.verifiedAccount,
recipient: recipient || req.verifiedAccount,
fee: (BigInt(amount) * 30n / 10000n).toString(),
createdAt: Date.now(),
updatedAt: Date.now(),
confirmations: 0,
requiredConfirmations: direction === 'deposit' ? 12 : 1,
};
// Save to database
await saveTransfer(transfer);
// Return deposit address for inbound transfers
const depositAddress = direction === 'deposit'
? getAnchorEvmAddress() // your anchor's EVM address
: getAnchorKeetaAddress(); // your anchor's Keeta address
res.json({
transferId: transfer.id,
depositAddress,
amount: transfer.amount,
status: transfer.status,
expiresIn: 3600, // 1 hour to complete deposit
});
}Step 5: Chain Monitoring
The critical piece — watch both chains for incoming deposits:
// src/monitor.js
import KeetaNet from '@keetanetwork/keetanet-client';
import { ethers } from 'ethers';
// Monitor Keeta for withdrawals (user sends to anchor)
export async function monitorKeeta(anchorAccount, client, onDeposit) {
let lastHead = null;
setInterval(async () => {
try {
const state = await client.getAccountState(anchorAccount);
const currentHead = state.head;
if (currentHead !== lastHead && lastHead !== null) {
// New blocks — check for incoming transfers
const history = await client.history(anchorAccount);
for (const block of history) {
if (block.type === 'receive' && !isProcessed(block.hash)) {
await onDeposit({
txHash: block.hash,
from: block.sender,
amount: block.amount,
token: block.token,
});
markProcessed(block.hash);
}
}
}
lastHead = currentHead;
} catch (err) {
console.error('Keeta monitor error:', err.message);
}
}, 5000); // poll every 5 seconds
}
// Monitor EVM chain for deposits (user sends to anchor's EVM address)
export async function monitorEvm(provider, anchorEvmAddress, onDeposit) {
// For ERC-20 tokens, listen to Transfer events
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const erc20Abi = ['event Transfer(address indexed from, address indexed to, uint256 value)'];
const contract = new ethers.Contract(USDC_ADDRESS, erc20Abi, provider);
contract.on('Transfer', async (from, to, value, event) => {
if (to.toLowerCase() === anchorEvmAddress.toLowerCase()) {
await onDeposit({
txHash: event.log.transactionHash,
from,
amount: value.toString(),
token: 'USDC',
blockNumber: event.log.blockNumber,
});
}
});
}Step 6: Executing the Cross-Chain Transfer
When a deposit is confirmed, send tokens on the destination chain:
// src/executor.js
async function executeKeetaSend(client, anchorAccount, recipient, amount, token) {
// Send tokens on Keeta (for deposit direction: ext → keeta)
await client.send(amount, recipient, token);
// Keeta has instant finality — once published, it's confirmed
}
async function executeEvmSend(wallet, recipient, amount, tokenAddress) {
// Send tokens on EVM (for withdrawal direction: keeta → ext)
const erc20Abi = ['function transfer(address to, uint256 amount) returns (bool)'];
const contract = new ethers.Contract(tokenAddress, erc20Abi, wallet);
const tx = await contract.transfer(recipient, amount);
const receipt = await tx.wait();
return receipt.hash;
}Confirmation Thresholds
Different chains have different finality guarantees:
Chain | Confirmations | Approx. Time | Why |
|---|---|---|---|
Keeta | 1 (instant) | < 1 second | Representative voting = instant finality |
Base | 12 blocks | ~24 seconds | L2 with fast blocks, but reorgs possible |
Ethereum | 32 blocks | ~6 minutes | Proof-of-stake finality |
Bitcoin | 6 blocks | ~60 minutes | Proof-of-work, probabilistic finality |
Persistent Forwarding Addresses
For recurring deposits, generate a unique address per user:
// Each user gets their own deposit address derived from a master key
export function getForwardingAddress(userKeetaAddress, masterSeed, index) {
// Derive a unique EVM account for this user
const wallet = ethers.HDNodeWallet.fromSeed(masterSeed)
.derivePath(`m/44'/60'/0'/0/${index}`);
// Store mapping: forwarding address → user's Keeta address
saveMapping(wallet.address, userKeetaAddress);
return wallet.address;
}Persistent forwarding means the user can deposit any time without initiating a transfer first — the anchor watches the address and auto-forwards.
Error Handling
Bridge anchors have unique failure modes:
Failure | Cause | Recovery |
|---|---|---|
Source deposit confirmed, dest send fails | Destination chain down or insufficient balance | Queue for retry, alert operator |
Source deposit reorged after dest credited | Source chain reorg | Track confirmation depth, only credit after threshold |
Anchor goes offline mid-transfer | Server crash | Persist all state, resume on restart |
Duplicate deposit detection | Block re-scan | Idempotent processing — check |
Security Considerations
Hot wallet management — the anchor holds funds on both chains. Minimize hot wallet balances; use cold storage for excess.
Rate limiting — cap maximum transfer sizes and daily volume per account.
Balance alerts — notify operators when hot wallet balances get low on either chain.
Audit logging — log every state transition for every transfer. You'll need this for reconciliation.
Next Steps
Publishing Metadata — register your bridge so the resolver discovers it
Anchor Business Models — fee structures for bridges
Case Studies — see how the Keeta EVM anchor handles real cross-chain transfers