Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Build a Cross-Chain Yield Router Agent

AI AgentsAdvanced40 min

A yield router watches a basket of cross-chain DeFi opportunities, compares the bridge cost to reach each one, and routes capital to the highest net-yielding destination. In this tutorial you will build one entirely from the command line using the tenzro-cli and the AgentKit template system. No Rust source code is required. You will provision an identity and wallet with tenzro join, register a machine agent with chain whitelisting, spawn the ref-yield-rebalancer-v1 template, compare live bridge fees via tenzro_getBridgeRoutes JSON-RPC calls, and watch the agent enforce delegation scopes before every dispatch.

What You'll Build

  • A human controller identity at KYC tier Enhanced via tenzro join
  • A machine yield-router agent with chain whitelist (ethereum, arbitrum, base) and 7-day time bound
  • A spawned instance of the ref-yield-rebalancer-v1 AgentKit template
  • Live fee comparison via tenzro_getBridgeRoutes across LayerZero V2, Chainlink CCIP, and deBridge DLN
  • APY-sorted opportunity routing with delegation enforcement before each bridge dispatch
  • A demo confirming oversized bridges are rejected and unsupported chains are refused

Why AgentKit Templates Beat Hand-Rolling Code

Cross-chain routing has two hard problems: knowing which adapter supports a given destination, and knowing the live fee for a payload of a given size. Tenzro's BridgeRouter solves both in one call: tenzro_getBridgeRoutes walks every registered adapter, asks it to quote, and returns a sorted list with the cheapest first. The adapters themselves use real on-chain calls: LayerZero quotes via EndpointV2.quote(), CCIP quotes via Router.getFee(), deBridge quotes via the live order-creation API. The ref-yield-rebalancer-v1AgentKit template wraps all of this into a single spawnable agent that you configure with a JSON file of yield opportunities. You don't need to import crates, write Rust, or manage a build toolchain.

Two Layers of Chain Filtering

You get chain restriction at two independent layers: the delegation scope's allowed_chains blocks the agent from even attempting an unsupported chain, and the BridgeRouter's adapter list determines which chains have a cost-quotable route. Both must allow the destination, otherwise the call fails. This is defense-in-depth: a misconfigured router cannot let the agent bridge to a chain the controller never approved.

Prerequisites

You need a running Tenzro node. If you are connecting to the public testnet, the RPC endpoint is https://rpc.tenzro.network. If you are running a local node, start it first:

cargo run --bin tenzro-node -- --role validator --listen-addr /ip4/0.0.0.0/tcp/9000

The CLI commands below assume a local node at localhost:8545. Replace with the testnet URL if needed.

Step 1: Join the Network

The tenzro join command provisions your human controller identity, creates an MPC wallet, and detects your hardware profile in one shot:

# One command provisions your identity + wallet + hardware profile
tenzro join
Identity provisioned: did:tenzro:human:a1b2c3d4-...
Wallet created:       0x7f3a...9c1e (Ed25519)
KYC tier:             Enhanced
Hardware:             aarch64, 16 cores, 64GB RAM

Confirm you have testnet tokens:

# Confirm the wallet has testnet TNZO
tenzro wallet balance

Per-VM balances. The tenzro wallet balance command shows your aggregate TNZO balance. For a detailed breakdown across all VM representations (native, wTNZO ERC-20, SPL, CIP-56), use tenzro token balance. The tenzro token CLI subcommand provides additional token-specific operations including cross-VM transfers, wrapping, and token creation.

If your balance is zero, hit the faucet:

# If the balance is zero, request testnet tokens
curl -s -X POST https://api.tenzro.network/faucet \
  -H "Content-Type: application/json" \
  -d '{"address":"0x7f3a...9c1e"}' | jq

Step 2: Register the Yield Router Agent Identity

Register a machine identity under your controller with a delegation scope that whitelists exactly three chains, caps per-transaction value at 500 USDC, and expires after 7 days:

# Register a machine identity under your controller identity
tenzro identity register \
  --type machine \
  --display-name "yield-router" \
  --capabilities "chain:ethereum,chain:arbitrum,chain:base,yield-route" \
  --max-tx-value 500000000 \
  --max-daily-spend 2000000000 \
  --allowed-ops "bridge,yield-route" \
  --allowed-chains "ethereum,arbitrum,base" \
  --time-bound-days 7
Agent identity registered:
  DID:              did:tenzro:machine:a1b2c3d4:e5f6a7b8-...
  Controller:       did:tenzro:human:a1b2c3d4-...
  Wallet:           0x4d2e...8f3a (Ed25519, auto-provisioned)
  Delegation scope:
    max_tx_value:   500 USDC
    max_daily_spend: 2,000 USDC
    allowed_ops:    [bridge, yield-route]
    allowed_chains: [ethereum, arbitrum, base]
    time_bound:     7 days from now

The --allowed-chains flag installs a hard whitelist on the delegation scope. Even if the node's BridgeRouter has an adapter for Fantom or Solana, the agent cannot bridge to those chains. The --max-tx-value is in 6-decimal USDC base units (500000000 = 500 USDC). The --time-bound-days 7 sets both the start (now) and end (7 days from now) automatically.

Step 3: Explore the AgentKit Template

AgentKit ships with pre-built agent templates for common DeFi patterns. List what is available:

# Browse available AgentKit templates
tenzro agent list-templates
ID                           | Category    | Description
-----------------------------|-------------|---------------------------------------------
ref-yield-rebalancer-v1      | DeFi        | Cross-chain yield router with bridge fee comparison
ref-portfolio-rebalancer-v1  | DeFi        | Portfolio drift detection and rebalance
ref-dca-v1                   | DeFi        | Dollar-cost averaging with MPP payments
ref-arb-v1                   | DeFi        | Cross-chain arbitrage with multi-leg routing

Inspect the yield rebalancer template to understand its parameters:

# Inspect the yield rebalancer template before spawning
tenzro agent get-template ref-yield-rebalancer-v1
Template: ref-yield-rebalancer-v1
Version:  1.0.0
Category: DeFi

Description:
  Autonomous yield router that watches a basket of cross-chain DeFi
  opportunities, compares bridge costs via LayerZero V2, Chainlink CCIP,
  and deBridge DLN, and routes capital to the highest net-yielding
  destination. Enforces TDIP delegation scopes before every bridge dispatch.

Required capabilities:
  - chain:ethereum, chain:arbitrum, chain:base
  - yield-route, bridge

Parameters:
  --source-chain    Source chain for bridge operations (default: ethereum)
  --opportunities   JSON file or inline JSON array of yield opportunities
  --dry-run         Compare fees and print routes without dispatching (default: true)
  --max-slippage    Maximum bridge fee as percentage of deploy amount (default: 2.0)

Step 4: Define Yield Opportunities

Create an opportunities.json file with three real-world DeFi opportunities — Aave on Ethereum, GMX GLP on Arbitrum, and Aerodrome on Base. The agent will sort them by APY descending so capital flows to the highest-yielding destination first:

[
  {
    "chain": "ethereum",
    "protocol": "Aave v3 USDC",
    "apy_bps": 380,
    "deploy_amount_usdc": 200
  },
  {
    "chain": "arbitrum",
    "protocol": "GMX GLP",
    "apy_bps": 1250,
    "deploy_amount_usdc": 250
  },
  {
    "chain": "base",
    "protocol": "Aerodrome USDC/cbBTC",
    "apy_bps": 720,
    "deploy_amount_usdc": 300
  }
]

Save this as opportunities.json in your working directory.

Step 5: Spawn the Agent Instance

The spawn-template command creates a running instance of the template, bound to your agent identity and configured with your opportunities. The --dry-run true flag means the agent will compare fees and prepare bridge requests but will not actually broadcast transactions:

# Spawn an instance of the yield rebalancer bound to your agent identity
tenzro agent spawn-template ref-yield-rebalancer-v1 \
  --agent-did did:tenzro:machine:a1b2c3d4:e5f6a7b8-... \
  --source-chain ethereum \
  --opportunities opportunities.json \
  --dry-run true \
  --max-slippage 2.0
Agent instance spawned:
  Instance ID:  yield-router-a1b2c3d4
  Template:     ref-yield-rebalancer-v1
  Agent DID:    did:tenzro:machine:a1b2c3d4:e5f6a7b8-...
  Source chain: ethereum
  Opportunities: 3 loaded
  Mode:         dry-run (no bridge dispatch)
  Status:       ready

Step 6: Compare Bridge Fees Manually

Before running the agent, you can inspect bridge fees yourself using the tenzro_getBridgeRoutes JSON-RPC method. This is the same call the agent makes internally for every cross-chain leg. It walks every registered adapter (LayerZero V2, Chainlink CCIP, deBridge DLN) and returns sorted fee quotes:

# Compare bridge fees across all adapters for ethereum -> arbitrum
curl -s http://localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tenzro_getBridgeRoutes",
    "params": {
      "source_chain": "ethereum",
      "dest_chain": "arbitrum",
      "payload_size": 64
    }
  }' | jq
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "routes": [
      {
        "adapter": "layerzero",
        "fee": "0.000842",
        "currency": "ETH",
        "estimated_time_secs": 120
      },
      {
        "adapter": "debridge",
        "fee": "0.001200",
        "currency": "ETH",
        "estimated_time_secs": 60
      },
      {
        "adapter": "ccip",
        "fee": "0.001350",
        "currency": "ETH",
        "estimated_time_secs": 900
      }
    ],
    "cheapest": "layerzero",
    "fastest": "debridge"
  }
}

The routes are sorted by fee ascending — the cheapest adapter comes first. Each adapter quotes using real on-chain calls: LayerZero uses EndpointV2.quote(), CCIP uses Router.getFee(), and deBridge uses the DLN order-creation API. If an RPC call fails, the adapter falls back to a static estimate.

Intra-network vs. external bridges. The bridge adapters (LayerZero, CCIP, deBridge) are for moving tokens between Tenzro and external chains like Ethereum mainnet, Arbitrum, or Base. For moving TNZO/wTNZO between Tenzro's own VMs (EVM, SVM, Canton), use tenzro token transfer --cross-vm instead. Cross-VM transfers route through precompile 0x1003 (CROSS_VM_BRIDGE), which is faster and fee-free compared to bridge adapters since the underlying native balance is shared via the Sei V2 pointer model.

Try the same for Base:

# Compare fees for ethereum -> base
curl -s http://localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tenzro_getBridgeRoutes",
    "params": {
      "source_chain": "ethereum",
      "dest_chain": "base",
      "payload_size": 64
    }
  }' | jq '.result.routes[] | {adapter, fee, currency}'

And confirm that unsupported chains return zero routes:

# Try an unsupported chain  no adapters will return routes
curl -s http://localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tenzro_getBridgeRoutes",
    "params": {
      "source_chain": "ethereum",
      "dest_chain": "fantom",
      "payload_size": 64
    }
  }' | jq '.result.routes | length'
# Returns: 0

Step 7: Run the Yield Routing Loop

Execute the spawned agent instance. The agent sorts opportunities by APY, skips same-chain deployments, compares bridge fees for each cross-chain leg, picks the cheapest adapter, enforces the delegation scope, and prepares the bridge request:

# Execute the yield routing loop
tenzro agent run-template yield-router-a1b2c3d4
[yield-router] Starting yield routing cycle...
[yield-router] Sorting 3 opportunities by APY (descending)

[yield-router] 1. GMX GLP on arbitrum (12.50% APY)  deploy 250 USDC
  Comparing bridge fees: ethereum -> arbitrum (64 bytes)
    layerzero   fee = 0.000842 ETH
    debridge    fee = 0.001200 ETH
    ccip        fee = 0.001350 ETH
  Cheapest adapter: layerzero @ 0.000842 ETH
  Delegation check: bridge 250 USDC... OK
  [dry-run] Would bridge 250 USDC via layerzero: ethereum -> arbitrum

[yield-router] 2. Aerodrome USDC/cbBTC on base (7.20% APY)  deploy 300 USDC
  Comparing bridge fees: ethereum -> base (64 bytes)
    layerzero   fee = 0.000910 ETH
    debridge    fee = 0.001100 ETH
    ccip        fee = 0.001420 ETH
  Cheapest adapter: layerzero @ 0.000910 ETH
  Delegation check: bridge 300 USDC... OK
  [dry-run] Would bridge 300 USDC via layerzero: ethereum -> base

[yield-router] 3. Aave v3 USDC on ethereum (3.80% APY)  deploy 200 USDC
  Same-chain deployment, no bridge required
  [dry-run] Would deploy 200 USDC directly to Aave v3 USDC

[yield-router] Cycle complete. 3 opportunities processed, 2 bridges prepared.

The output shows the agent's decision process for each opportunity: fee quotes from all three adapters, the cheapest pick, the delegation check, and the prepared bridge request. GMX GLP on Arbitrum (12.50% APY) gets routed first because it has the highest yield. Aave on Ethereum skips the bridge entirely since it is a same-chain deployment.

Step 8: Confirm the Delegation Boundaries

First, inspect the agent's delegation scope to see the hard limits:

# Test the delegation boundary with an oversized amount
# The agent's max_tx_value is 500 USDC — try 1000 USDC
curl -s http://localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tenzro_resolveIdentity",
    "params": {
      "did": "did:tenzro:machine:a1b2c3d4:e5f6a7b8-..."
    }
  }' | jq '.result.delegation_scope'
{
  "max_transaction_value": 500000000,
  "max_daily_spend": 2000000000,
  "allowed_operations": ["bridge", "yield-route"],
  "allowed_chains": ["ethereum", "arbitrum", "base"],
  "time_bound": {
    "start": "2026-04-08T00:00:00Z",
    "end": "2026-04-15T00:00:00Z"
  },
  "active": true
}

Now spawn a second instance with an opportunity that exceeds the 500 USDC per-transaction cap:

# Spawn with an opportunity that exceeds the per-tx cap
tenzro agent spawn-template ref-yield-rebalancer-v1 \
  --agent-did did:tenzro:machine:a1b2c3d4:e5f6a7b8-... \
  --source-chain ethereum \
  --opportunities '[{"chain":"arbitrum","protocol":"Oversized Test","apy_bps":2000,"deploy_amount_usdc":1000}]' \
  --dry-run true

tenzro agent run-template yield-router-oversized-test
[yield-router] Starting yield routing cycle...
[yield-router] Sorting 1 opportunities by APY (descending)

[yield-router] 1. Oversized Test on arbitrum (20.00% APY)  deploy 1000 USDC
  Comparing bridge fees: ethereum -> arbitrum (64 bytes)
    layerzero   fee = 0.000842 ETH
  Cheapest adapter: layerzero @ 0.000842 ETH
  Delegation check: bridge 1000 USDC... BLOCKED
    DelegationViolation: transaction value 1000000000 exceeds max 500000000
  Skipping opportunity.

[yield-router] Cycle complete. 1 opportunities processed, 0 bridges prepared.

The delegation scope rejected the 1000 USDC bridge because it exceeds the 500 USDC per-transaction cap. The agent logged the violation and skipped the opportunity cleanly — no on-chain footprint, no wasted gas. This is the same enforce_operation check that runs in the production path. Combined with the zero-route result for unsupported chains like Fantom (Step 6), you have defense-in-depth: the delegation scope blocks unauthorized amounts, and the BridgeRouter blocks unauthorized destinations.

What You Learned

Next Steps