simulateTransaction on a $0 mainnet fork → Receipt → a fail-closed
signing gate. Only a passing Receipt reaches a signer, and the signer is never Gecko.
What a Receipt asserts
- Would this transaction LAND against a snapshot of on-chain state (
status:pass/fail), viasimulateTransaction(sigVerify:false,replaceRecentBlockhash:true,commitment:"processed"). - Why it wouldn’t, as a categorical
revert_class— a stable string vocabulary:slippage,custom_program_error:<code>,insufficient_funds,account_error,other. Never a fabricated dollar number. - Compute units consumed (
units_consumed). - Best-effort deltas:
sol_delta(post − pre lamports for the first tracked account) andtokens_received(only when a token account is tracked and decodable — otherwiseNone, never guessed). - A
network_labelhonesty caveat, present on every Receipt.
What a Receipt does NOT do
- It does not predict price or slippage — only whether the tx lands against a snapshot.
- A fork/RPC result is NOT mainnet — the
network_labelsays so; a surfpool-fork Receipt is never presented as mainnet truth. - It says nothing about send-time blockhash validity — the simulation uses
replaceRecentBlockhash:true, so the real transaction must take a fresh blockhash at sign time (the builder/lander’s job); a passing Receipt can still expire before it is signed. - It does not quote a priority fee —
SetComputeUnitPricedefaults to 0 in the simulated bundle; landing under mainnet load needs a fee the operator (or builder) supplies. The Receipt’sunits_consumedis the honest input for the CU limit, not the price. - It stores nothing — the Receipt is returned to the caller and persisted nowhere (the control-plane invariant). No payload, pubkey, or log line is written.
- It never signs and never broadcasts —
simulateTransactiononly. No keypair, nosendTransaction. Mainnet broadcast stays a separate, human-run step.
Two paths to a Receipt
Path A — Gecko runs it
Hand a built plan (with
fee_recipient supplied) to the simulate MCP tool on the
program surface; it builds, simulates, and returns the Receipt. One tool call, no
glue code.Path B — self-serve
plan_buy returns a simulate recipe block — fill fee_recipient, POST build_url
to get the tx, then run simulateTransaction yourself. You own the loop; Gecko
supplies the correct account set and the recipe.fee_recipient stays an honest gap: Gecko will not guess it; the caller
supplies it.
The proof
Two live side-by-sides, both on a surfpool mainnet fork — a mainnet-backed state snapshot, not mainnet — simulation only, $0, nothing signed, nothing broadcast, nothing stored.
The Pump.fun run reverts on the buyer’s uninitialized ATA. The Gecko bundle passes
because it carries a curve-quoted
max_sol_cost, the recovered bonding_curve_v2, the
buyback fee-recipient remaining accounts, an idempotent-ATA prelude, and a compute
budget. The Meteora run exercises the full native-SOL bundle: both ATAs idempotent, wSOL
wrap, the swap with the three bitmap-selected bin_array remaining accounts the IDL
never names, and a CloseAccount unwrap — one unsigned simulated transaction.
Compute-unit numbers are measured per run against a fork snapshot and can vary
slightly with on-chain state. The stable claim is the side-by-side verdict — the naive
path’s revert class vs Gecko’s pass. The
base_factor derivation row is a separate
result and carries no CU number.surfpool on PATH and a
mainnet RPC), and their verbatim output is recorded in
docs/proofs.md.
Recording an outcome (opt-in)
A run can append its categorical outcome — status, revert family plus public code, compute units, slot, network category, and a values-free recipe hash; never a pubkey, amount, or log — to a segregated series. The default is record nothing; it takes an explicitrecord_to opt-in.