Operating your agent
Your agent will do OK out of the box. To move funds well, give it a policy - what to shield, when to send, when to hold. Concrete instructions beat abstract ones every time.
How instructions actually flow
Your agent reads three layers when it works with RAIL20:
- The RAIL20 CLI (or raw HTTP API) - what commands exist, how signatures work, how the burner flow runs. Reference-level, not policy.
- Any agent skill or framework you're using (LangChain tool, custom function, cron script) - the loop and glue.
- Your conversation with the agent - the policy. What amounts, what recipients, when to move, when to stop.
Layer 3 is the highest-priority layer. The CLI is reference; you set the policy. If you tell your agent "never send more than 10 USDC per hour to any single address," that overrides any neutral defaults it might otherwise adopt.
Set a policy before it moves funds
The most useful thing you can do is hand your agent a one-paragraph policy before it does anything on-chain. Be operational - not "be careful," but exactly what "careful" means in specific spots.
Example - private treasury manager
You manage a private treasury on RAIL20 (Base). Rules: - Shield deposits within 10 minutes of arrival at the public wallet. - Keep at least 5 USDC unshielded for emergency gas / manual overrides. - Never send more than 25 USDC in a single tx or 100 USDC per rolling 24 hours. - Any send > 10 USDC to a new recipient (not seen in the last 30 days) requires my explicit confirmation. - Log every send to a local audit file; include tx hash, recipient, amount, timestamp. - If a swap or bridge fails, run `rail20 recover` immediately and report the sweep back to me. - Never touch the ETH balance below 0.01 (that's my gas floor).
Example - A2A payment agent
You pay two peer agents privately for services rendered: - Payments are triggered by invoices from Peer A (0xA1..) or Peer B (0xB2..). No other recipients. - Amount must match invoice within ±1%. If it doesn't, decline and alert me. - Bundle daily invoices into one send per peer per day where possible (fee is flat 1 USDC + 0.35%, batching saves money). - Use `rail20 send` from the USDC pool. Never use the ETH pool. - If private USDC balance drops below 5, shield 20 USDC from the public wallet before continuing. - Never expose the recipient list, invoice contents, or amounts in any log that could leak publicly.
Example - cross-chain rebalancer
You rebalance private USDC across Base, Arbitrum, and Ethereum daily: - Target ratio: 50% Base, 30% Arbitrum, 20% Ethereum (of total private + public). - Only rebalance if any chain drifts more than 10% from target. Otherwise sit. - Use `rail20 bridge` from base_usdc. Minimum bridge is 3 USDC (bridge fee is flat 1 USDC + 0.35% - anything smaller loses too much). - Never bridge more than 50 USDC in a single tx. Split into multiple bridges if needed. - After a bridge shows SUCCESS, wait 5 minutes before triggering another to avoid solver capacity issues. - If any bridge shows REFUNDED, alert me and stop rebalancing until I clear.
Why isn't my agent doing what I asked?
Three common reasons, in order of frequency:
- Your instruction was too abstract. "Be conservative" doesn't mean anything to the model. Be operational: "shield within 10 minutes of arrival," "never send > 25 USDC per tx." Give it thresholds, amounts, time windows.
- The session restarted. If your agent runs in a cron or a fresh chat, it forgets the policy. Either paste it back every session, or save it to a policy file the agent reads on start (e.g.
~/.agent/rail20-policy.txt). - The model reverted to defaults. After many turns or a summarization, the policy can drift. Re-state it concretely, and include 2-3 examples of what to do in specific spots ("if a swap fails at step 5, run
rail20 recoverand report the sweep tx").
Why did my transaction fail?
Four failure modes cover almost every fund-movement problem on RAIL20. Each has a specific fix.
1. Fee guard rejection - "withdrawal amount too small"
✗ Error: withdrawal amount must exceed 2× the fee
The relayer refuses sends smaller than 2 × fee. Fee is flat + 0.35%. For the USDC pool that's flat 1 USDC, so minimum viable send is ~2.0035 USDC. For the ETH pool it's flat 0.00025 ETH, min ~0.000502 ETH. This is by design - relayers can't profitably front proofs for dust.
Fix: increase the amount, or batch multiple small payments into one send.
2. Indexer lag - "balance shows 0 right after deposit"
private balance 0.0 USDC (moments after a successful deposit tx)
Private balances are reconstructed from on-chain notes. After a deposit or send tx, the indexer needs ~5-15 seconds to pick up the new commitment and expose it via /api/balance. During that window, balance can transiently look stale.
Fix: use rail20 balance --pool usdc --wait. It polls the indexer for up to 30 seconds until the balance is non-zero. In your policy, always use --wait after a tx that changes balance.
3. Stranded burner - "swap failed mid-flow"
✗ Error at step 5 of swap. Funds are safe on burner 0xF5d0..…
Swap runs a 6-step burner flow. If any step past the private withdraw fails (network glitch, gas spike, RPC hiccup), funds sit on a burner wallet - deterministic from your signature, so they are never lost.
Fix: run rail20 recover. It scans the first 20 burner slots and sweeps any USDC + non-dust ETH back to your public wallet. Fully idempotent - safe to run any time. In your policy, always run recover after any swap error.
4. Insufficient private balance
✗ Error: withdraw exceeds available notes (asked N, have M)
Private balance is a set of notes, not a single number. If your agent asks to send more than the sum of unspent notes, the proof fails to build.
Fix: run rail20 balance before sending, and either shrink the amount or shield more via rail20 deposit. In your policy, always check balance before initiating a send if the amount could plausibly exceed reserves.
Talking to your agent during work
Once your agent is operating, keep nudging it:
- "Show me a heartbeat" - status snapshot: private/public balance both pools, last N txs, any pending swaps or bridges.
- "Pause all sends until I clear" - operational safety brake without shutting down the agent.
- "Why did you skip that invoice?" - agent explains its reasoning against your policy.
- "Reduce your daily cap to 20 USDC" - policy update mid-session.
- "Any stranded burners?" - triggers a
rail20 recoverdry check.
Hard limits
Some things the CLI and relayer won't do, no matter how you prompt:
- No private key exfiltration. The CLI never transmits your key. Only the derived signature is sent to the relayer. Anyone claiming otherwise is looking at the wrong code path.
- No sub-fee-guard sends. The relayer rejects amounts below
2 × fee. Your agent cannot bypass this by any prompt. - No cross-signature account access. The signature derives one account. Different key = different account = different balance. There is no admin override.
- No proof forgery. Only nullifiers spent by the note owner's key clear. The relayer builds the proof but cannot spend on the user's behalf.
Debugging your agent
When something's off, check these first.
Agent isn't executing any commands
Two common causes. (1) The session died - cron-style runners can stall silently. Restart it, or ask the agent to print a heartbeat every N minutes. (2) It's waiting on your confirmation for a large tx that hit the policy threshold. Ask "any pending confirmations?" - it should list them.
"Transaction reverted" or unexplained on-chain failure
Usually one of three things:
- Allowance propagation lag. On load-balanced public RPCs, an
approvetx can be confirmed while a fresheth_callto the same RPC still sees the old allowance. The CLI polls for allowance propagation before broadcasting the follow-up tx. If you're doing this manually, add a similar poll. - Chain-realistic gas. Base runs at ~0.001-0.05 gwei, Robinhood at ~0.047 gwei (its RPC may suggest ~1.6 gwei, ~34x too high). Ethers defaults to 1.5 gwei, which drains small burner top-ups before the tx completes. The CLI caps
maxFeePerGasper chain (0.05 gwei Base / 0.1 gwei Robinhood). Manual integrations should too. - Reshield gas underestimate. The reshield
transact()verifies a Groth16 proof on-chain and uses ~1.35M gas. If you cap gasLimit too low, it reverts near completion. The CLI usesestimateGas+ 20% buffer.
Agent lost a burner's private key
It didn't. Burners are deterministic from your signature: keccak256("rail20-burner-v2:" + keccak256(signature) + ":" + nonce). Slots 0-19 are always scannable. Run rail20 recover from any machine that has your key - the same burners will re-derive and any funds swept.
"rail20 latest says I'm on an old version"
Update: npm install -g @rail20/cli@latest. Newer versions may include gas fixes, indexer improvements, or new commands that your agent's policy assumes.