Concrete, copy-paste recipes: real commands with real output, and how to fold rome into an agent, a shell script, or CI. All output below is verbatim from Hadrian (200010).
- Every command, with real output
- Integrate into an AI agent (MCP)
- The agent grounding loop
- Start an app (
rome new) - Fund a wallet from another chain (
fund/bridge) - Bridge out of Rome (
bridge --to) +activate - Prove it works —
rome verify - Shell & scripting recipes
- Use it in CI
- End-to-end: build a price-reading contract, grounded by rome
$ rome facts chain hadrian
{
"chainId": 200010,
"name": "Rome Hadrian",
"network": "devnet",
"status": "live",
"rpcUrl": "https://hadrian.testnet.romeprotocol.xyz/",
"explorerUrl": "https://via-hadrian.testnet.romeprotocol.xyz/",
"romeEvmProgramId": "RPTWwELXAY4KC9ZPHhaxp7Sq1hHtU3HNEgLbSegCcWf",
"nativeCurrency": { "name": "Rome Hadrian", "symbol": "USDC", "decimals": 18 },
"solana": { "cluster": "devnet", "rpc": "https://api.devnet.solana.com" }
}Resolves by id, name, or slug: 200010, hadrian, 200010-hadrian, "Rome Hadrian" all return the same chain. An unknown or ambiguous input fails loudly with the known set — it never guesses.
$ rome facts tokens hadrian
{
"chainId": 200010,
"tokens": [
{ "address": "0xeeee…eeee", "mintId": "4zMMC9srt5Ri…DncDU", "symbol": "USDC", "decimals": 18, "kind": "gas" },
{ "address": "0xd4cc34b6…ef28", "mintId": "4zMMC9srt5Ri…DncDU", "symbol": "wUSDC", "decimals": 6, "kind": "spl_wrapper" },
{ "address": "0x8c2c1486…be7e", "mintId": "6F5YWWrUMNpe…ifWs", "symbol": "wETH", "decimals": 8, "kind": "spl_wrapper" },
{ "address": "0x1dece035…4201", "mintId": "So1111…1112", "symbol": "wSOL", "decimals": 9, "kind": "spl_wrapper" }
],
"note": "Token entries omit assetRef — match a wrapper to its underlying by shared mint (mintId); the gas token's wrapper shares its mint."
}Note the gas token (USDC) and its wrapper (wUSDC) share a mintId — that's how you pair them (see the scripting recipe below).
$ rome facts gas hadrian
{
"chainId": 200010,
"gasPriceWei": "10403793960",
"note": "eth_estimateGas can over-predict by a large factor; Rome charges the exact gas used… A plain native-token transfer costs ~1.48M gas (not 21k)."
}$ rome facts programs devnet
{
"network": "devnet",
"programs": {
"splToken": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"wormholeCore": "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5",
"cctpTokenMessenger": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
…
}
}$ rome cookbook cpi-recipe
{
"precompiles": { "cpi": "0xff…08", "helper": "0xff…09", "withdraw": "0x42…16", "system": "0xff…07" },
"accountRules": [
"The accounts array must be non-empty.",
"The operator and the program_id must NOT appear in the accounts array.",
"To sign as your contract, pass HELPER.pda(address(this)) as the signer — the CPI precompile signs as msg.sender (the calling contract's PDA), not tx.origin, so a router cannot sign a user's PDA."
],
"encoders": { "invoke": "encodeInvoke (from @rome-protocol/sdk)", "invokeSigned": "encodeInvokeSigned (from @rome-protocol/sdk)" }
}$ rome cookbook patterns lending
[ { "goal": "Lending / borrow",
"core": "a Solidity core; Solana users arrive via a synthetic sender",
"repo": "aerarium",
"guide": "developer-guides/dual-lane-app",
"tags": ["lending","borrow","supply","money-market","compound","lend"] } ]No goal → the full index (AMM → rome-dex, CPI → cardo, from-home → appia, oracle → rome-oracle-gateway, scaffold → create-rome-app).
$ rome cookbook errors gas
[ { "symptom": "eth_estimateGas returns a huge value (often 10-50× the real charge)…",
"cause": "Rome charges the EXACT gas used; the estimate is a loose upper bound.",
"fix": "Don't hard-fail or size budgets off the estimate. A native transfer is ~1.48M gas, not 21k." } ]No query → the full taxonomy (Custom(0)/AccountLocked, CpiProhibitedInIterativeTx, UnknownInstruction(N), forge --skip-simulation, Solana-lane Custom(1), …).
$ rome doctor hadrian --address 0x1Fc3…
{
"chain": { "id": 200010, "name": "Rome Hadrian", "network": "devnet", "status": "live" },
"checks": [
{ "name": "chain-live", "ok": true, "detail": "status=live" },
{ "name": "program-configured", "ok": true, "detail": "RPTWwELX…" },
{ "name": "rpc-reachable", "ok": true, "detail": "https://hadrian…/ (gasPrice …)" },
{ "name": "wallet-funded", "ok": true, "detail": "… wei (USDC)" }
],
"ok": true
}$ rome tx hadrian 0x8d99…
{
"status": "success",
"receipt": { "gasUsed": "0x1607b0", "from": "0x…", "to": "0x…", "blockNumber": "0x…" },
"solanaSettlement": ["5Lwke7W1…"],
"explorer": "https://via-hadrian.testnet.romeprotocol.xyz/tx/0x8d99…",
"note": "Confirmed. solanaSettlement lists the Solana tx(s) that settled this EVM tx."
}Rome has no debug_trace*; tx maps the EVM hash to its Solana settlement via rome_solanaTxForEvmTx.
$ rome preset foundry hadrian
{
"tool": "foundry", "chainId": 200010, "filename": "foundry.toml",
"config": "[rpc_endpoints]\nrome-hadrian = \"https://hadrian.testnet.romeprotocol.xyz/\"",
"notes": ["forge script --skip-simulation …", "Rome charges exact gas; estimateGas over-predicts …", "…"]
}Give your coding agent grounded Rome facts so it stops guessing addresses and selectors.
1. Install (repo-first; npm publish pending) — clone + link. A plain npm install -g github:… fails: npm prepares this package's github: dependencies without their own node_modules (#25):
git clone https://github.com/rome-protocol/rome-cli
cd rome-cli && npm install && npm install -g .2. Register the MCP server in your client's config. You don't run or host anything — the client launches rome mcp on demand.
Claude Code (~/.claude/settings.json or project .mcp.json):
{ "mcpServers": { "rome": { "command": "rome", "args": ["mcp"] } } }Claude Desktop / Cursor — the same block in their MCP config. Skipping the install? The npx form works too:
{ "mcpServers": { "rome": { "command": "npx", "args": ["-y", "github:rome-protocol/rome-cli", "mcp"] } } }3. Verify the tools are live:
rome mcp # starts the stdio server; your client lists the READ tools: facts_chain,
# facts_tokens, facts_contracts, facts_gas, facts_balance, facts_programs,
# cookbook_cpi_recipe, cookbook_patterns, cookbook_errors, call, doctor, tx, preset
# (actions — new/deploy/send/fund/bridge/activate/verify — are CLI-only, never on MCP)4. Use it in a prompt. Now the agent can call the tools instead of hallucinating:
"Deploy my lending contract to Hadrian. Use
facts_chainfor the RPC + gas token,facts_contractsfor the addresses, andcookbook_cpi_recipeif you touch a Solana program. Don't hardcode anything."
The agent calls facts_chain(chain: "hadrian"), gets the real RPC + romeEvmProgramId, and proceeds on facts.
The pattern that makes an agent reliable on Rome — look up, then write:
1. rome cookbook patterns <what I'm building> → which example repo + architecture
2. rome facts chain <chain> → RPC, program id, gas token (no hardcoding)
3. rome facts contracts <chain> → the addresses I'll wire against
4. rome cookbook cpi-recipe → the CPI account-rules (if calling Solana)
5. …write the code against those exact values…
6. rome facts gas <chain> → sanity-check before sizing a tx
Concrete run for "I'm building a lending app":
$ rome cookbook patterns lending # → repo: aerarium, core: Solidity + synthetic sender
$ rome facts chain hadrian # → rpcUrl, romeEvmProgramId, gas = USDC
$ rome facts tokens hadrian # → wUSDC 0xd4cc34b6…, wETH, wSOL (the reserves)The agent now writes against aerarium's pattern with Hadrian's real RPC and the real wrapper addresses — nothing invented.
The scaffold front door. Wraps create-rome-app (the canonical dual-lane scaffolder — a Vault contract + both lanes + a Vite UI), then adds what the scaffolder can't know: your chain, resolved from the registry and pre-wired into the app's .env. Keyless — it signs nothing (the key stays out until you fund/deploy).
$ rome new my-app --chain hadrian
{
"app": "my-app",
"chainId": 200010,
"chainName": "Rome Hadrian",
"next": [
"cd my-app && npm install",
"# fund the wallets in .env (gas is USDC — bridge it in; no faucet):",
"rome fund hadrian --from base-sepolia --amount 1",
"npm run deploy # deploy the Vault to Rome Hadrian",
"npm run demo # the funded dual-lane proof (MetaMask + Phantom → one Vault)",
"rome verify hadrian # the works-gate, any path"
]
}The next steps are the whole lifecycle in this CLI's own commands — scaffold → fund → deploy → prove. Templates live in create-rome-app (today: the dual-lane Vault, i.e. the bring-your-idea path); as per-path templates land there, new grows matching flags.
The from-home path: you hold USDC on another chain and want onto Rome. fund bridges it in as native gas; bridge --intent wrapper brings it in as wUSDC. Both use Circle CCTP, orchestrated through @rome-protocol/sdk's bridge module — you sign only the source-chain burn; Rome's sponsor pays the settle.
These are actions: CLI-only, and they read ROME_EVM_KEY from the environment (never a flag, never logged, never on MCP).
Preview first with --dry-run — quotes the route and shows exactly what you'd sign, spending nothing:
$ rome fund hadrian --from base-sepolia --amount 0.5 --dry-run
{
"dryRun": true,
"route": "usdc-cctp-to-rome",
"amountIn": "500000",
"amountOut": "500000",
"fee": { "bps": 0, "absolute": "0", "asset": "USDC" },
"etaSeconds": 1100,
"plannedTxs": [
{ "stepN": 1, "to": "0x036CbD…dCF7e", "description": "Approve TokenMessenger to spend USDC" },
{ "stepN": 1, "to": "0x8FE6B9…2DAA", "description": "Burn USDC via CCTP, mintRecipient = user's Rome account" }
]
}Drop --dry-run to execute: rome signs + broadcasts the two source txs, signs the trustless-settle authorization (gas intent only), registers the transfer, then polls to completion — printing the transfer id, outcome, and source tx hashes.
rome fund hadrian --from base-sepolia --amount 0.5 # → native gas on Rome
rome bridge hadrian --from base-sepolia --amount 0.5 --intent wrapper # → wUSDC on RomeSupported source chains come from the registry's bridge config for the target Rome chain — Base Sepolia, Arbitrum Sepolia, Polygon Amoy, Avalanche Fuji, Monad Testnet, Sepolia. Resolve a source by id, name, or slug (84532, "base sepolia", base-sepolia). CCTP standard attestation takes ~15–20 min, so a real transfer isn't instant; the command polls until it lands.
Holding ETH instead? Add --asset eth — it rides Wormhole and lands as wETH on Rome (never gas; gas is USDC, so --intent doesn't apply):
rome bridge hadrian --from sepolia --amount 0.002 --asset eth # ETH → wETH on Rome (~15 min VAA)You sign one wrap-and-transfer tx on the source; Rome's sponsor completes the transfer on the Solana side once the VAA is ready.
The bridge-api base defaults to the devnet orchestrator; override with
--bridge-api <url>orROME_BRIDGE_API.
The reverse of fund / bridge --from: you hold wUSDC on Rome and want USDC back on another chain. bridge --to <dest> burns your wUSDC on Rome via RomeBridgeWithdraw (CCTP v2) and hands you a claim handle for the destination. --from = in, --to = out (exactly one; fund is always in).
The responsibility splits by chain — this is the design, not a limitation:
- Rome side — orchestrated for you. You sign one burn tx; the engine registers the transfer and polls until Circle attestation is ready. (Rome runs a sponsor for the inbound settle intent; outbound carries no settle.)
- Destination side — yours. Rome does not sponsor the destination mint. You call
MessageTransmitterV2.receiveMessage(message, attestation)on the destination — which needs gas on that chain. The command returns the transmitter, the CCTP domain, and the transfer id so you (or Circle's portal / the bridge-api) can complete it. Delivery is permissionless (destinationCaller = 0), so anyone can submit it.
Inbound is deliberately frictionless — no per-user account is needed. The first time you bridge out, CCTP's deposit_for_burn creates a per-burn messageSentEventData account funded by your external-auth PDA, so that PDA must hold lamports (~15M reserve per burn). rome activate <chain> funds it once (~2 USDC, via the on-chain SimpleActivator); it's idempotent — it skips with no spend if you're already activated. bridge --to checks this first (a pure EVM read of the PDA lamports via the account_lamports precompile) and points you here rather than letting the burn revert deep inside CCTP.
$ rome activate hadrian
{ "address": "0x1Fc3…", "pda": "BTbRPi8n…", "alreadyActivated": true, "lamports": "26099680" }$ rome bridge hadrian --to base-sepolia --amount 0.1 --dry-run # quote + planned burn, no spend
$ rome bridge hadrian --to base-sepolia --amount 0.1
{
"route": "usdc-cctp-from-rome",
"burnTxHash": "0xc4549892…",
"destinationChainId": 84532,
"claim": {
"yourResponsibility": true,
"status": "ready",
"transmitter": "0xE737…E275",
"domain": 6,
"note": "Claiming on Base Sepolia … call MessageTransmitterV2.receiveMessage(message, attestation) … (needs gas on Base Sepolia). The bridge-api tracks this transfer at id=txf_…"
}
}Destinations are the same registry CCTP chains as sources (resolve by id / name / slug). --recipient sets the destination address (default = your address).
ETH out: --asset eth burns your wETH on Rome (two txs: approve + burn) and exits via Wormhole to Ethereum only (--to sepolia; the CLI refuses any other destination). Same claims-are-yours rule — redeem the VAA on Ethereum when it's ready, and guard with isTransferCompleted: re-redeeming an already-completed VAA reverts with a misleading gas error. First outbound still needs rome activate.
- The real bridge is on-chain.
RomeBridgeWithdraw.burnUSDC(egress) + Circle CCTP v2 do the work. The bridge-api (rome-protocol/rome-bridge-api) is an off-chain orchestrator + tracker — it quotes the route and follows the transfer; it holds no funds and cannot move yours. - Inbound (
--from): burn on the source L2 → Circle attestation → Rome's sponsor settles on Rome. You sign only the source burn; no activation needed. - Outbound (
--to): activate (first time) → burn wUSDC on Rome → Circle attestation → you claim on the destination. No settle authorization, no destination sponsor. - Base override for the orchestrator:
--bridge-api <url>orROME_BRIDGE_API(defaults to the devnet orchestrator).
The keystone: prove a contract actually works on Rome, driven from both lanes. verify --path solidity deploys a probe, sets a value from the EVM lane (submitRomeTx) and the Solana lane (submitRomeTxSolanaLane), and asserts the same contract answered on each — the litmus test, runnable.
A funded action: needs ROME_EVM_KEY + ROME_SOLANA_KEY (env-only), CLI-only, never on MCP.
$ rome verify hadrian --path solidity
{
"path": "solidity",
"probe": "0x368744e9…",
"checks": [
{ "lane": "evm", "wrote": "42", "read": "42", "ok": true },
{ "lane": "solana", "wrote": "43", "read": "43", "ok": true }
],
"ok": true
}ok: true means one contract on Rome answered correctly whether an EVM wallet or a Solana wallet drove it — the dual-lane promise, verified on-chain.
Brought a Solana program? The gate is different: prove an EVM-lane call reaches it through the CPI precompile. verify --path solana-program deploys a thin CPI wrapper (its constructor self-provisions the contract's external-auth PDA), then an EVM-lane ping drives the SPL Memo program via CPI. A successful EVM receipt is the proof — a failed CPI reverts the tx. This path needs only ROME_EVM_KEY (no Solana key — the EVM lane is the whole story).
$ rome verify hadrian --path solana-program
{
"path": "solana-program",
"probe": "0x206d7101…",
"program": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
"evmTx": "0x82aae391…",
"cpiLanded": true,
"ok": true
}Add --solana-rpc <url> for the deep check: verify resolves the EVM tx to its Solana settlement (rome_solanaTxForEvmTx) and confirms the memo landed in the program's logs ("memoConfirmed": true).
Coming from another chain? This gate proves the whole journey: USDC bridges in (as wUSDC), works on Rome, and bridges back out.
$ rome verify hadrian --path from-home --from sepolia --amount 0.2
{
"path": "from-home",
"legs": {
"in": { "ok": true, "txHash": "0x…", "landed": true, "wusdcDelta": "200000" },
"act": { "ok": true, "hash": "0x…" },
"out": { "ok": true, "burnTxHash": "0x…", "claimReady": true }
},
"ok": true
}Three legs, in order, failing fast: in (CCTP wrapper intent — waits through Circle attestation, ~15–20 min, and asserts the wUSDC actually landed) → act (a real wUSDC transfer via submitRomeTx — the asset is usable on Rome) → out (bridge --to back to the source chain, asserted to attestation-ready + a claim handle). The destination claim is your step, as always. Prereqs (checked up front with clear errors): ROME_EVM_KEY; USDC + gas on the source chain; gas on Rome; an activated account (rome activate — the out-leg burns).
rome prints JSON — pipe it to jq.
Get a chain's RPC for a deploy script:
RPC=$(rome facts chain hadrian | jq -r '.rpcUrl')Pair the gas token with its wrapper by shared mint (the assetRef gotcha, solved):
GAS_MINT=$(rome facts tokens hadrian | jq -r '.tokens[] | select(.kind=="gas") | .mintId')
WUSDC=$(rome facts tokens hadrian | jq -r --arg m "$GAS_MINT" \
'.tokens[] | select(.kind=="spl_wrapper" and .mintId==$m) | .address')
echo "$WUSDC" # → 0xd4cc34b67c805d472b5a709a22a1037f6b16ef28Grab a Solana program id:
rome facts programs devnet | jq -r '.programs.wormholeTokenBridge'Look up an EVM contract's live address before wiring it (contracts is an array; each entry has versioned addresses):
# what's deployed:
rome facts contracts hadrian | jq -r '.contracts[].name'
# the live address of one contract:
rome facts contracts hadrian | jq -r \
'.contracts[] | select(.name=="ERC20SPLFactory") | .versions[] | select(.status=="live") | .address'
# → 0x86149124d74ebb3aa41a19641b700e88202b6285Fail a pipeline early if a fact drifts, instead of debugging a broken deploy later.
Assert the chain is live before deploying:
STATUS=$(rome facts chain "$CHAIN" | jq -r '.status')
[ "$STATUS" = "live" ] || { echo "::error::$CHAIN is not live ($STATUS)"; exit 1; }Pin an expected address (catch registry drift):
GOT=$(rome facts tokens hadrian | jq -r '.tokens[] | select(.symbol=="wUSDC") | .address')
[ "$GOT" = "$EXPECTED_WUSDC" ] || { echo "::error::wUSDC moved: $GOT"; exit 1; }Emit the RPC into the build env:
echo "ROME_RPC=$(rome facts chain "$CHAIN" | jq -r '.rpcUrl')" >> "$GITHUB_ENV"rome is read-only and needs no keys, so it's safe in any CI step.
You want a Solidity contract that reads a price feed. Let rome route and ground you:
$ rome cookbook patterns oracle
[ { "goal": "Price feeds in a contract",
"core": "Chainlink AggregatorV3Interface over Pyth / Switchboard",
"repo": "rome-oracle-gateway",
"guide": "products/oracle-gateway" } ]→ Read rome-oracle-gateway; the feeds are behind the standard Chainlink AggregatorV3Interface.
$ rome facts contracts hadrian | jq -r '.contracts[].name' # what's deployed to wire against
$ rome facts chain hadrian | jq -r '.rpcUrl' # the RPC your deploy script points atNow your contract imports IAggregatorV3Interface from @rome-protocol/rome-solidity, wires the feed address from rome facts contracts, deploys against the RPC from rome facts chain, and you sized nothing off a guess. When you add a write path, use @rome-protocol/sdk's submitRomeTx (the CLI stays read-only).
See also: README.md for the quick start, and docs/ARCHITECTURE.md for how it's built and the security model.