Skip to main content

Writing Your First Intent: A Routes CLI Walkthrough

Routes CLI tutorial: install, configure, sign, submit, and monitor your first cross-chain stablecoin intent in under ten minutes. Code included.

Written by Eco
Updated today

If you've read about intents and solvers and you want to see the execution model with your own hands, the fastest path is to sign one. This Routes CLI tutorial walks through the full flow — install the tooling, configure chains and tokens, sign a cross-chain stablecoin intent, submit it to the solver network, and monitor the fill — in about ten minutes of real time. No mainnet funds required for the dry-run mode.

By the end you'll have a working local setup, a signed intent in your shell history, and a mental model for how to integrate intent submission into your own backend. This is a product-anchored developer tutorial, so every snippet is copy-paste runnable.

Prerequisites

You need three things on your machine:

  • Node 18+ and pnpm installed (npm install -g pnpm).

  • An EVM wallet private key for a test account — you can generate one with cast wallet new from Foundry's tooling if you don't have one handy.

  • A small test balance of USDC or USDT on any supported source chain. Eco Routes supports 15 chains; the full list is in the Routes product overview. For this walkthrough we'll use Base as the source and Arbitrum as the destination — both are fast and cheap.

If you've never interacted with Base or Arbitrum before, Eco's primers on the Ethereum Virtual Machine and each L2 are useful background reading. Otherwise, proceed.

Step 1: Install the Routes CLI

Clone the Routes CLI repo on GitHub and install dependencies. The pnpm link step makes the routes command available globally, which is handy for poking around interactively.

git clone https://github.com/eco/routes-cli.gitcd routes-clipnpm installpnpm buildpnpm linkroutes --help

The help output enumerates the top-level commands: publish, status, config, and a few others. publish is what we'll use to sign and submit an intent; status lets us track the fill; config manages defaults so we don't type flags on every invocation. If you prefer API-first integration, the Eco developer documentation covers the equivalent surface for backend code.

Step 2: Configure your wallet and chains

Set your private key as an environment variable (never paste it on the command line directly):

export ROUTES_PRIVATE_KEY=0x...your_test_key...routes config set-default --source base --destination arbitrum

The CLI reads chain definitions from a bundled config that covers the full 15-chain set — Ethereum, Optimism, Base, Arbitrum, HyperEVM, Plasma, Polygon, Ronin, Unichain, Ink, Celo, Solana, Sonic, BSC, and Worldchain. Tokens are similarly pre-configured for USDC, USDT, USDC.e, oUSDT, USDT0, USDbC, and USDG. If you need to verify which token addresses the CLI is targeting, routes config show tokens --chain base prints the resolved list.

For a deeper view of how route configurations select between messaging layers, Eco's explainers on Hyperlane Routes and Native Routes are worth a skim — the CLI defaults to "auto" and picks the best rail per lane, but you can override if your team requires a specific finality channel. For general Hyperlane context, see the Hyperlane documentation.

Step 3: Write the intent

This is the canonical flow: input asset and amount, output asset and minimum, recipient, deadline. The interactive wizard walks you through it, but the non-interactive form is clearer for scripting.

routes publish \  --source base \  --destination arbitrum \  --input-token USDC \  --input-amount 10 \  --output-token USDT \  --min-output 9.98 \  --recipient $(routes wallet address) \  --deadline 60s \  --dry-run

--dry-run simulates the full flow without actually broadcasting. The CLI fetches quotes from the solver network, shows you the competing bids, and tells you which one would win. This is your sanity check — always dry-run an intent before committing real funds.

The output looks roughly like this:

[routes] intent hash: 0x8f3a2c...1b9e[routes] fetching solver quotes...[routes]   solver A: 9.994 USDT  (win)[routes]   solver B: 9.991 USDT[routes]   solver C: 9.988 USDT[routes] estimated fill time: 14s[routes] collateral posted: 11 USDC equivalent[routes] DRY RUN — no transaction broadcast.

Read the output the way you'd read a limit order book. Three solvers bid, the tightest wins, the collateral model kicks in to guarantee delivery. For the full mechanics of what's happening under the hood, the pillar explainer on intents and solvers walks through the handshake step by step, and the existing stablecoin intent walkthrough covers a slightly different test case.

Step 4: Submit the intent for real

Drop the --dry-run flag and run again. The CLI will ask for final confirmation, sign the intent with your private key, and broadcast it to the solver network.

routes publish \  --source base \  --destination arbitrum \  --input-token USDC \  --input-amount 10 \  --output-token USDT \  --min-output 9.98 \  --recipient $(routes wallet address) \  --deadline 60s

What you're signing is an ERC-7683 cross-chain intent. The signature commits you to the inputs and the minimum output; it does not commit you to a specific solver or path. The network handles that. If you want to read the standard directly, the official EIP-7683 text is surprisingly readable for an EVM standard.

The CLI returns an intent ID that looks like intent_0x8f3a2c...1b9e. Save it — you'll use it to check status.

Step 5: Monitor the fill

Most intents fill in under 30 seconds. You can either tail the status interactively or poll programmatically.

routes status intent_0x8f3a2c...1b9e --follow

Status progresses through the four solver-network phases: posted (intent is live in the auction), committed (a winning solver has posted collateral and is executing), filled (funds have arrived in the destination address), and settled (collateral has been released, intent is closed). If you want to verify each phase on-chain, the destination chain's block explorer — for Base that's BaseScan, for Arbitrum Arbiscan — will show the fill transaction with the solver's address as sender.

When status hits filled, check your destination address balance. The agreed output amount should be there, no bridge limbo, no manual claim step. If something goes wrong — solver times out, collateral is slashed, network issue — the CLI surfaces the failure mode and any automatic refund. Failed-fill handling is covered in more depth in the solver rebalancing writeup, which explains why a solver might drop a quote at the last second and how the network recovers.

Step 6: Integrate into your backend

The CLI is a convenience wrapper around the Routes API. Once you're comfortable signing intents from the shell, the same operations are available programmatically. Here's a minimal Node sketch — exact shape depends on the version of the SDK you're using, so check the repo README.

import { RoutesClient } from '@eco/routes-sdk';const client = new RoutesClient({ privateKey: process.env.ROUTES_PRIVATE_KEY });const intent = await client.publish({  source: 'base',  destination: 'arbitrum',  inputToken: 'USDC',  inputAmount: '10000000',     // base units, 10 USDC  outputToken: 'USDT',  minOutput:   '9980000',      // base units, 9.98 USDT  recipient:   '0x...your_address...',  deadlineSeconds: 60,});const fill = await client.waitForFill(intent.id);console.log('filled', fill.txHash);

The programmatic path is what you'd wire into a treasury rebalancer, a payment processor, or an agent framework. Eco's pieces on the broader patterns — stablecoin treasury netting, API-first treasury, and AgentKit stablecoin routing — describe real-world integrations.

Debugging the common pitfalls

A handful of issues show up often enough in first-time Routes CLI tutorials that they're worth flagging.

No solver quotes returned. Most likely the source/destination/token combination doesn't have active solver coverage. Try a mainstream pair first (USDC Base ↔ USDT Arbitrum is always active) and confirm the network is reachable. If you need a lane that isn't currently covered, flag it — the solver supply curve is actively managed.

Insufficient allowance. The CLI uses permit-style approvals where possible, but some chains still need a one-time ERC-20 allowance. If you see an allowance error, run routes approve --token USDC --chain base --amount max once and retry. The mechanics of permit-based approvals are covered in Eco's Permit1 guide.

Deadline too short. A 60-second deadline is tight for cross-chain flows involving L1 settlement. Push to 120s if you see timeout errors on slower rails.

Nonce or chain-ID mismatch. If you've been testing against multiple chains from the same wallet, the CLI sometimes caches state. routes config clear-cache and retry.

What the CLI is doing behind the scenes

A simple mental model helps when you're debugging. When you run routes publish, four things happen under the hood, in roughly this order.

1. Intent construction. The CLI assembles a structured message from your flags — inputs, outputs, recipient, deadline — and normalizes them to the ERC-7683 format. This is deterministic; given the same inputs, you always get the same intent hash.

2. Signing. The CLI signs the intent with the private key from your environment variable using EIP-712 typed-data signing (the same mechanism as permit-based approvals). The signature is what the solver network uses to authenticate the intent — no transaction is broadcast yet.

3. Solver auction. The signed intent is submitted to the Routes network, where active solvers race to quote. The CLI's dry-run mode shows you this auction in real time; production mode accepts the winning bid automatically unless you pass --min-solvers N to require a minimum number of competing bids.

4. Settlement. The winning solver executes the fill on the destination chain, locking collateral and broadcasting the output transaction. The CLI polls for confirmation and surfaces the status stream. If the solver fails, collateral is slashed and the CLI retries with the next-best bidder — all of this is explained in more depth in Eco's intent solver primer.

Holding this flow in your head makes every error message interpretable. An "auction failed" error means phase 3 dropped. A "fill timeout" means phase 4 stalled. A "signature rejected" means your private key doesn't match the claimed signer address. Debug starts with figuring out which phase broke.

Beyond the tutorial

Once the first intent works, you'll want to do at least three more things.

First, experiment with the other route configurations. Try forcing a Hyperlane Route versus a Native Route on the same lane and compare quotes — the differences tell you something about which rail is most capital-efficient for your flow.

Second, check out the solver-selection logic. The CLI exposes the competing quotes; your production code can read them and make policy decisions (always take the best price, or always take a specific solver, or weight by historical reliability). The solver economics breakdown explains why you might actually want to do this.

Third, wire the status stream into your observability. For a backend doing many intents, you want a consistent signal when fills complete, stall, or fail. Eco's pieces on cross-chain transaction tracking and settlement reconciliation cover how to think about this at scale.

The walkthrough you just did is the minimum viable test. The interesting product-building starts when you begin composing intents into larger workflows — payroll runs, rebalances, agent-driven transfers. That's where the intent model stops being a cleaner bridge and starts being a genuinely new execution primitive.

FAQ

Do I need mainnet funds to try the Routes CLI? No — --dry-run simulates the full flow, fetches live solver quotes, and tells you what would happen without broadcasting. Use it freely to learn the interface before committing real funds.

How is writing an intent different from calling a bridge? A bridge call is a specific transaction to a specific contract. An intent is a signed outcome — send X, receive at least Y, by time Z — and the network picks the path. Eco's comparison of intents vs bridges covers the architectural implications.

Which chains and tokens does the Routes CLI support? Fifteen chains (Ethereum, Optimism, Base, Arbitrum, HyperEVM, Plasma, Polygon, Ronin, Unichain, Ink, Celo, Solana, Sonic, BSC, Worldchain) and seven stablecoins (USDC, USDT, USDC.e, oUSDT, USDT0, USDbC, USDG). Coverage varies slightly by chain — BSC has Liquidity Manager disabled, Worldchain is partially active.

What's the difference between Routes CLI and Routes API? The CLI is the fastest way to experiment from a shell. The API is what you integrate into backend code. Both produce identical intents — the CLI is just a UX layer on top of the API.

What happens if my intent fails? If a winning solver fails to deliver, their collateral is slashed and the network either refunds you on the source chain or assigns a backup solver. You don't have to file a support ticket; the guarantee is enforced by the protocol.

Related articles

Did this answer your question?