Imported from rome-protocol/cardo (
AGENTS.md). Install upstream withnpx skills add rome-protocol/cardo. Copyright stays with the author.
AGENTS.md — building on Rome with an AI coding agent
Rome is EVM chains that run natively inside the Solana runtime. Your Solidity/EVM app executes inside a Solana program and can call Solana programs atomically (CPI). Two lanes reach the same chain and the same state: MetaMask/EVM tooling, and a Solana wallet (Solflare, for example).
This file gives you the facts, organized by what you're starting from. Find your starting point, apply the rules that bite on every path, and read the example repo closest to what you're building. Vanilla-EVM habits produce plausible-but-wrong code here — reads (eth_call, balances, logs) are standard; the differences are in writes, gas, tooling, CPI, and architecture.
First, the mental model — three facts everything rests on
- EVM inside Solana. Your Solidity runs inside a Solana program; from Solidity you can invoke any Solana program (SPL Token, Meteora, a program you wrote) atomically, in one transaction, via the CPI precompile.
- One state, two lanes. MetaMask (an EVM key) and a Solana wallet such as Solflare (a Solana key) drive the same contracts and the same state. A Solana user signs with their Solana key — no EVM keypair; Rome derives their EVM address (a synthetic address) from that key. An EVM user's on-chain identity is a Rome-derived PDA (
external_auth). - SPL is ERC-20, automatically. Any SPL token is already an ERC-20 on Rome — the same account, no bridge and no wrapped-asset hop. An LP or position token minted as an SPL mint is spendable as an ERC-20 too, and vice-versa, once the wallet has granted the token's wrapper (rule 4).
Find your starting point
You have Solidity — a contract, or an EVM app to fork
Deploy it as it is, with standard Hardhat or Foundry (one caveat: forge script needs --skip-simulation; forge create, cast, and Hardhat work normally). The same contract serves EVM users (MetaMask) and Solana users (Solflare, for example) — you don't write per-audience code, and nothing about your Solidity changes. When you want Solana liquidity or data, call a Solana program via CPI (see the rules below).
Read: compound-on-rome-comet and rome-aave-v3 — Compound v3 and Aave v3 running unchanged.
You have a Solana program
It keeps doing what it already does for your Solana users — no changes. To open it to EVM (and other-chain) users, write a thin Solidity wrapper that CPIs your program: the accounts array and instruction data are the same layout your program already expects. Make your instructions authority-agnostic — act on whichever authority signs — so the signer can be a Solana wallet pubkey or your wrapper's own PDA. The precompile signs as msg.sender (the wrapper's own PDA), not tx.origin; an EVM user's external_auth PDA signs only when the user calls the precompile directly.
Read: cardo — drives Jupiter, Meteora, Marinade, and Mango from an EVM account.
You have one lane and want to open the other — the Parity Pattern
Keep one source of truth, written authority-agnostic, and open the other lane to it. This comes in both shapes (see Which side is your core? below): a native Solana program + thin EVM router, or a Solidity core that Solana users reach via a synthetic sender. Either way a Solana wallet user and a MetaMask user act on the same reserves/state — any contract between the user and Solana (your router, or a token wrapper) signs as its own PDA, never the user's, so it moves a user's tokens only through an SPL delegation the user grants (rules 3 and 4). Because the LP/position token is an SPL mint, it's automatically an ERC-20, so a position opened on one lane is spendable on the other. Keep the account set lean so each call fits one Solana v1 transaction (up to 64 accounts and 4 KB), and test both lanes. Read: rome-dex (AMM — native core + router) and aerarium (lending — Solidity core + synthetic sender).
You're building greenfield — bring your idea
Use both sides from day one: one pool both audiences trade, one market both borrow from, an oracle that brings Solana prices into the EVM. Decide the architecture up front — see Which side is your core? below. Resolve everything from the registry; use the SDK for writes.
Read: rome-dex, appia, rome-oracle-gateway. Scaffold: rome new my-app --chain hadrian (wraps create-rome-app; pre-wires the chain and prints the fund → deploy → demo → verify lifecycle).
Your users are on their home turf — reach Rome from another chain
Your users don't move — from their home chain (an L2, Solana, …) they reach your Rome app without leaving. The bridge is on-chain: RomeBridgeWithdraw (egress from Rome) + the rome-evm settle_inbound_bridge program (inbound credit, authorized by the user's own signed EIP-712 intent). Transport is Circle CCTP (USDC) or Wormhole. The off-chain rome-bridge-api orchestrates it: quote a route → verify the source tx → sponsor the settle fee.
Read: appia (a from-home app), rome-bridge-api (the orchestrator; see its docs/BRIDGE_API_ARCHITECTURE.md).
Which side is your core?
Which side holds the core logic is a soft, per-app judgment — weigh existing code/assets, team expertise, security posture, tooling, performance, and how each audience uses it. There's no hard rule.
- (a) Solidity core; Solana users arrive via a synthetic sender. Often fits a Solidity contract you already have (especially hardened/audited), standard EVM tooling, or straightforward logic. A Solana user's signature runs as an EVM tx from their Rome-derived synthetic address — no EVM key. Example: aerarium.
- (b) Native Solana program core; EVM users arrive via a thin CPI router. Often fits an existing Solana program, or when running the core natively matters. Example: rome-dex.
For example, performance might tip it: native can be much cheaper than the EVM interpreter for heavy logic (one swap we measured ran ~6× cheaper) — consider it if you have a hot compute-heavy path. It's one input, not a rule.
Getting funded — USDC is the gas token; bridge it in
Every Rome chain's gas token is USDC (Circle). There is no Rome faucet. Fund a wallet by bridging USDC into Rome from a chain where you already have it: get testnet USDC on a source chain (Sepolia, an L2, or Solana) from that chain's own faucet, then bridge to Hadrian, the testnet (the on-chain settle_inbound_bridge, orchestrated by rome-bridge-api; CCTP for USDC). One command: rome fund <chain> --from sepolia --amount 1. Holding ETH instead? rome bridge <chain> --from sepolia --amount 0.002 --asset eth lands it as wETH (Wormhole). Bridging out (--to) is also one command — note the first outbound needs a one-time rome activate <chain> (~2 USDC), and the destination claim is your own step.
The rules that bite on every path (different from vanilla EVM)
- Every write goes through the SDK. On the EVM lane use
submitRomeTx(not rawwagmi/ethers/viemwriteContract/sendTransaction— Rome writes have specific fee + submission semantics). On the Solana lane (a Solana wallet driving your EVM app) usesubmitRomeTxV1: the proxy composes one Solana v1 transaction per call, the SDK checks it before the wallet signs, and the wallet is asked twice (a readable authorization, then the transaction). Reads stay vanilla. - Gas: use the estimate, never a hardcoded 21,000. Creating Solana accounts is what makes a write expensive. Measured on Hadrian: a native transfer cost 1,086,961 gas to a never-used address and 10,001 to an existing one; a wUSDC transfer to a never-used address cost 2,544,921, and a 10-byte contract deploy 1,147,921.
eth_estimateGasincludes the account creation, and Rome charges the gas actually used, which in these runs never exceeded the estimate.submitRomeTxpads the estimate 1.3x, and when a write reverts under estimation (some CPI-heavy writes do) it sends a fixed gas ceiling instead. - Calling Solana from Solidity (CPI — the differentiator). Precompiles: CPI
0xFF…08, Helper0xFF…09, Withdraw0x42…16. The account rules agents get wrong: the accounts array must be non-empty; the operator and the program_id must NOT appear in it; to sign as your contract useHELPER.pda(address(this))as the signer (the precompile signs asmsg.sender, so a router contract cannot sign a user's PDA). Full ABI + per-selector billing: the precompile reference (docs). - Grant each SPL wrapper once per wallet. An ERC-20 wrapper over an SPL mint (
wUSDC,wETH, …) moves a user's tokens as the SPL delegate of their token account. When the wrapper'sisEnabled(user)returns false, the wallet — a MetaMask address, or a Solana user's synthetic address (provisioned first, see the SDK section) — sendsapprove_spl(wrapper, 2^64-1, mint)straight to the Helper precompile, where the mint is the wrapper'smint_id(). Without it everytransferortransferFromout of that wallet reverts with SPL error0x4. - Never hardcode addresses — read the registry. Chain ids, RPC URLs, contract addresses, token mints, and Solana program ids all come from
@rome-protocol/registry(or theromeCLI/MCPfacts_chain/facts_tokens/facts_contractstools). Hardcoded values drift and break across deploys. - Test both lanes with a fresh wallet. A feature must work on the EVM lane (MetaMask) and the Solana lane (Solflare, for example). Verify each with a brand-new wallet and a tiny amount before claiming done. The Solana lane needs a wallet that signs Solana v1 transactions: on devnet, Solflare does and Phantom does not yet.
- When a tx fails, use the taxonomy + the cross-VM map. Rome surfaces specific failures (rent-starved pool payer or StateHolder, emulation-vs-simulation mismatches, nonce races). Match them against the taxonomy:
rome cookbook errors <query>. To see the Solana settlement of a Rome tx, map it withrome tx <chain> <hash>(raw RPC:rome_solanaTxForEvmTx).
The SDK — @rome-protocol/sdk
The TypeScript SDK (rome-sdk-ts) is your write path and your CPI toolkit: submitRomeTx + fee sizing, PDA/ATA derivation, CPI invoke/invoke_signed encoders, precompile bindings, and a /bridge module. For the Solana lane, submitRomeTxV1 mirrors submitRomeTx: a Solana wallet drives your EVM app with no EVM key, one Solana v1 transaction per call. buildFundLeg/buildSweepLeg move value in and out (the synthetic holds nothing at rest), but submitRomeTxV1 does not provision a fresh synthetic for you: check isSyntheticProvisioned and send buildCreatePdaCall(synthetic) through it once, before any value-moving call. A call too large for one transaction is relayed by the proxy and paid from the synthetic's own gas balance, which fundSyntheticGas tops up from the wallet's USDC; a relayed call that reverts does not throw, so check receipt.status. submitRomeTxSolanaLane is the earlier, legacy-transaction path. Use the SDK for every write and every Solana-from-Solidity call rather than hand-rolling calldata.
Your tool — the rome CLI + MCP server
rome-cli is the dev CLI + MCP server built for exactly this file's reader (one-shot: npx github:rome-protocol/rome-cli#v0.11.0 <command>; durable: clone the repo at a tag (git clone --branch v0.11.0 https://github.com/rome-protocol/rome-cli), then npm install && npm install -g . — a plain npm i -g github:… fails, and an unpinned npx can run an old cached copy). Reads run on both surfaces, no keys: facts (chain/tokens/contracts/gas/balance/programs) · cookbook (patterns / cpi-recipe / errors) · call · doctor · tx · preset (foundry/hardhat config). Register the MCP server once (after the durable install, so rome is on your PATH) — {"mcpServers": {"rome": {"command": "rome", "args": ["mcp"]}}} — and the tools are facts_chain, facts_tokens, facts_contracts, cookbook_patterns, cookbook_errors, doctor, tx, preset, …. Actions are CLI-only and key-gated (ROME_EVM_KEY from the environment, never a flag, never on MCP): new · deploy · send · fund · bridge · activate · verify. The MCP surface can never sign or move funds.
Verify before you claim done
Before saying a change works: check RPC config, confirm every write goes through the SDK (submitRomeTx / submitRomeTxV1), and run a both-lane smoke against the Hadrian testnet with a fresh wallet and a tiny amount. The one-command works-gate is rome verify <chain> --path <solidity|solana-program|from-home> — dual-lane parity for a contract, an EVM-lane call driving a Solana program via CPI, or the full bridge-in → act → bridge-out round trip. For a scaffolded app, npm run demo is the funded dual-lane proof.
