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

/simulate

POST

Estimate fees and output for a transfer

/initiate

POST

Start a cross-chain transfer (signed request)

/status/:id

GET

Check transfer status

/forwarding

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 status

Step 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 isProcessed(txHash)

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

More in Anchor Guides · All documentation