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:

  1. The RAIL20 CLI (or raw HTTP API) - what commands exist, how signatures work, how the burner flow runs. Reference-level, not policy.
  2. Any agent skill or framework you're using (LangChain tool, custom function, cron script) - the loop and glue.
  3. 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

policy.txt
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

policy.txt
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

policy.txt
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:

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:

Hard limits

Some things the CLI and relayer won't do, no matter how you prompt:

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:

  1. Allowance propagation lag. On load-balanced public RPCs, an approve tx can be confirmed while a fresh eth_call to 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.
  2. 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 maxFeePerGas per chain (0.05 gwei Base / 0.1 gwei Robinhood). Manual integrations should too.
  3. 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 uses estimateGas + 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.

Rough guide: never let an agent move more than 5-10% of your total treasury per day without human review. RAIL20 hides the movements, but a runaway agent moving 100% of your balance still moves 100% of your balance. Privacy is not a substitute for policy.