You got sandwiched. Your swap landed between a searcher's buy and their sell, you paid for it, and every receipt you can pull agrees that you were robbed. None of them can prove the order it happened in. That is the strange gap this post is about. A transaction receipt carries the block number, the gas used, the logs, the status. It does not carry the transaction's position inside the block. The transactionIndex field your RPC node hands back is the node's word, not something a contract on another chain can verify. So "A ran before B" is a fact everyone can see and nobody can prove. index41 is a contract on Creditcoin that proves it, and pays a sandwiched victim out of a bond when it holds. Repo: https://github.com/edycutjong/index41 · Live: https://index41.edycu.dev The position is the shape of the proof Creditcoin's Attestcoin Protocol lets a contract verify that a specific Ethereum transaction is included in a specific block. You hand a precompile the encoded transaction, a merkle authentication path, and a continuity proof that ties the block to a checkpoint Creditcoin has attested. The precompile re-derives the root and either accepts or reverts. A merkle authentication path is normally an opaque list of sibling hashes. But walking it from the leaf to the root also answers a question at each level: was I the left child, or the right one? Write those answers down and you have a bit string. Read it least-significant-bit first and you have an integer. That integer is the leaf's position in the tree, which is the transaction's position in the block. // src/audit.ts — the off-chain decode, kept as a check against the precompile export function indexFromLaterality(proof: TransactionMerkleProof): number { let index = 0; proof.siblings.forEach((sibling, bit) => { if (sibling.isLeft) index |= 1 << bit; }); return index; } For the sandwich in the demo, the path for the front-run reads RLLLRRRR leaf to root. With L → 1 and R → 0 that is 01110000, least-significant first +2 +4 +8, position 14. The victim decodes to 15, the back-run to 16. Nothing wrote those numbers anywhere. They fall out of the geometry of a proof that was already needed for inclusion. Creditcoin's verifier precompile exposes exactly this as calculateTxIndex, a view, so on-chain it costs nothing beyond a few gas per level. It is not in the docs. The docs cover verifyAndEmit, because that is what nearly everyone needs. The precondition, and why it is load-bearing Here is the part that would have bitten me if I had trusted the function name. calculateTxIndex verifies nothing. It folds the is_left flags of whatever siblings it is handed into an integer and returns it. Hand it a made-up path and it will cheerfully tell you position 14. You can read that in the precompile source at a pinned commit. So the index is only meaningful for a proof that has already been verified, and the contract has to enforce the order itself: // contracts/src/Index41.sol function _proveLeg(Claim calldata c, uint256 i) private returns (ProvenLeg memory p) { LegBundle calldata leg = c.legs[i]; p.queryId = _computeQueryId(c.chainKey, c.blockHeight, leg.merkleRoot, leg.siblings); if (processedQueries[p.queryId]) revert QueryAlreadyProcessed(p.queryId); processedQueries[p.queryId] = true; bool verified = _verifyProof( c.chainKey, c.blockHeight, leg.encodedTransaction, leg.merkleRoot, leg.siblings, c.lowerEndpointDigest, c.continuityRoots ); if (!verified) revert VerificationFailed(i); p.txIndex = VERIFIER.calculateTxIndex( INativeQueryVerifier.MerkleProof({root: leg.merkleRoot, siblings: leg.siblings}) ); _decodeLeg(c, i, leg.encodedTransaction, p); } Verify first, revert on failure, and only then read the index from the same proof. I asked the Creditcoin team whether this use of an undocumented surface was sane, and their engineering team's answer was the precondition stated plainly: safe to use, as long as you call it after verify or verifyAndEmit, because it only computes the position from the proof being presented. That is exactly the shape above. One Creditcoin transaction does the whole ruling: three verifyAndEmit calls sharing one continuity proof, three calculateTxIndex calls, the assertion front < victim < back, the harm computation, and the payout. It cost 1,092,100 gas, which is 1.456% of the 75,000,000 block cap, so the "will three verifications fit in one transaction" question that worried me on day one turned out to have three orders of magnitude of headroom. The wall I could not climb, and what I did instead The intuitive definition of a sandwich's harm is the victim's loss against the pool's reserve ratio just before the front-run. I spent a while trying to prove that, and it cannot be done here, for a structural reason rather than a version gap: Attestcoin commits transaction history, not state. The merkle root is over encoded transactions and receipts. Post-state is never committed, so there is nothing to counterfactually compare against, and a contract that claimed to would be lying. The honest substitute is the attacker's realized profit: front-run amountIn against back-run amountOut, both read from Swap logs that sit inside the verified bytes. It is a smaller number than the counterfactual loss. It has a property the counterfactual does not: the payout can never exceed what was proven. For the demo sandwich in Ethereum mainnet block 25,764,741, that came to 219,708 wei, and the bond paid exactly that. Numbers Everything below is one run of npm run bench against the real hosted prover and the real CC3 testnet. There is no mock path in the repo. path n p50 p95 min max hosted proof fetch 20 638 ms 1,214 ms 622 ms 3,395 ms local proof build, no prover 5 171,045 ms 386,790 ms 154,986 ms 386,790 ms end-to-end prove → on-chain ruling 5 16,874 ms 17,243 ms 12,696 ms 17,243 ms At n=5 the p95 is simply the slowest observation, and the n is small on purpose because each end-to-end trial lands a real transaction. The hosted prover is 268× faster at the median, and the project does not depend on it: the local path, built with the SDK's own merkle hashers, returns the same root, 0x362ca563…76c16. If the prover goes away, the proof still gets built. It just takes three minutes. The contract suite is 145 Foundry tests across 6 suites at 100% line, branch and function coverage on the contract sources, plus 54 Playwright end-to-end tests against the demo. What it does not do It proves ordering and realized profit for a sandwich whose three legs share a pool. It does not prove the victim's counterfactual loss, for the reason above, and it never will on this protocol. It is on CC3 testnet. The bond is testnet CTC. The relay that fetches proofs and submits claims is the project's own key so far. The court is permissionless, but nobody outside the project has run a ruling through it yet. The demo page reads its three positions live from the precompile's own TransactionVerified logs on every load, and it says so on the page. If the node is unreachable it falls back to a recorded real read and labels that too. If you have a CC3 testnet wallet and want to be the first external ruling, the repo's README has the three-command path. I would genuinely like to see a claim I did not submit. Repo: https://github.com/edycutjong/index41 · Live demo: https://index41.edycu.dev