simulateTransaction against a node → Receipt → a binding over the
exact bytes. Only a passing Receipt with an exact binding hands back a transaction. 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,commitment:"processed"). The money paths (prepare_purchase,prepare_instruction,plan_swap) simulate the exact bytes withreplaceRecentBlockhash:false, so the binding covers the blockhash the caller is about to sign over. - 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 is state-specific and it expires. An exact binding dies with its blockhash,
about 60 seconds; the prepare result carries the budget as
expires.last_valid_block_heightandexpires.blocks_remaining. A receipt taken before the buyer decided is a receipt for bytes nobody will sign. Re-running is free. - 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. No keypair exists on any Gecko path. The one send path,
submit_transaction, relays signed bytes only after they verify at exact strength against this receipt’s binding, and refuses without one.
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. On the hosted orquestra surface, prepare_purchase and
prepare_instruction do this against a public mainnet RPC by default.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.
On mainnet, the same receipt has preceded 50 landed transactions as of 2026-09-01. See
A real transaction.
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.
The binding
A passing receipt on a money path comes back with two more fields:next_step orders the work:
sign, then verify_signed_transaction (signed bytes against the binding), then submit.
submit_transaction runs that check itself, at exact strength, before anything reaches
the wire, and refuses without a binding. Verifying after broadcast is a post-mortem;
verifying before it is a decision.
What the binding does not say: that this is the purchase the user wanted. It says the
signed bytes are the bytes a passing simulation attested, against the state observed at
that slot, for as long as that blockhash lives.