DEV Community

Cover image for The swap said 0.000502 WSOL. The wallet got 0.000494 SOL.
Swift Bot Labs
Swift Bot Labs

Posted on Originally published at swiftbotlab.github.io

The swap said 0.000502 WSOL. The wallet got 0.000494 SOL.

After a swap, most people open an explorer to answer three questions: did it work, what did I get, what did it cost. Explorer pages are organised around different questions: which instructions ran, against which accounts, in what order. That is the right layout for debugging a program and a poor one for checking your own money.

Here is a real Jupiter swap of DICE for SOL. Solscan's headline for it reads:

Swap 24,785,431.639664 DICE for 0.000502417 WSOL

The wallet's SOL balance went from 10.714646341 to 10.715140524, a change of +0.000494183 SOL. The headline is the pool's payout in wrapped SOL, after the pool's 1.25% fee but before two more deductions: Jupiter's 0.1% fee and the network fee. Nothing on the page is false. It just isn't what the person received.

The rest of the cost picture is scattered the same way. The swap paid three fees, about $0.0015 in all. On Solscan, the pool's fee appears only in the program's event log, one fee is displayed as 0.0₆502 and copies as 0.06502 (129,522x too large), and on a phone the network fee sits about 1,263px down the page.

We rebuilt that page as a receipt for a case study, then made the idea work on any transaction. (Concept only. We are not affiliated with Solscan.)

What it does

Paste a signature or an explorer link into the live tool. You get what you gave, what you got, what it cost, where every lamport went, and how it happened (route and programs). Every rounded figure opens its exact value and base units, and copies exactly.

It is static HTML and JavaScript. It calls a free public Solana RPC for getTransaction and Jupiter for token names and today's SOL price. Nothing you paste goes anywhere else. The Content-Security-Policy is default-src 'none'; script-src 'self', with connect-src limited to those three hosts, and the DOM is built with textContent, so no innerHTML ever touches chain data. Token names are attacker-controlled, so an unverified token can't borrow a well-known symbol: a mint that calls itself "USDC" is shown by its mint address.

How the receipt model works

Balance deltas are the ground truth. Every figure comes from preBalances/postBalances and preTokenBalances/postTokenBalances in the transaction meta, never from the headline instruction. Amounts are BigInt end to end, with no floats between RPC and screen. The wallet's SOL change is post minus pre, and its wrapped-SOL accounts count as part of its SOL, because they hold the same money.

Protocol-aware labels sit on top. Parsed instructions, inner ones included, are used to name the lines: sale proceeds, an aggregator fee, a Jito tip, a rent deposit, a send to someone else. We cover Jupiter (including multi-hop routes), Pump.fun, Raydium, Orca and Meteora, plus plain SOL, SPL and Token-2022 transfers. Instructions name the lines. The balances audit them.

Honesty rules. A story is told only when:

  • SOL is conserved across the whole transaction (every account's change sums to minus the fee);
  • tokens are conserved per mint, except for mint and burn instructions and Token-2022 withheld fees;
  • the itemised lines add up, to the unit, to the wallet's real change;
  • no unrecognised program had write access to accounts in the transaction.

Fail any check and the page reduces to what it can prove: balance changes, the fee and the programs involved, with the reason in plain words. A plain receipt beats a confident wrong one. For the swap above, the case-study ledger reads:

  0.000508778 SOL   sale value, before the pool's fee
- 0.000006361 SOL   pool fee (1.25%)
- 0.000000502 SOL   Jupiter fee (0.1%)
- 0.000007732 SOL   network fee (5,000 base + 2,732 priority)
= 0.000494183 SOL   wallet: 10.714646341 -> 10.715140524
Enter fullscreen mode Exit fullscreen mode

We checked the receipts against 1,229 real mainnet transactions: Jupiter multi-hop routes, Pump.fun, Raydium, Orca, Meteora, plain SOL/SPL/Token-2022 transfers and failed transactions. Four cases from that set show why the rules look the way they do.

Four edge cases

Tokens that charge a transfer fee. A Token-2022 mint can have the token program hold back a fee on every transfer. Say the sender's balance drops by 100 and the recipient's rises by 98: a naive "tokens are conserved" check calls the data broken. Our rule is that the only allowed gap is a shortfall on a Token-2022 mint, and the receipt names it ("this token charges its own transfer fee"). Any other gap, or any surplus, reduces the receipt.

"Also sent", outside the swap. Aggregator transactions often contain a transfer to another wallet that isn't part of the trade. If a "fee" is larger than a twentieth of the trade, it isn't a fee. It is output being sent on. We list it under "Also sent", never count it as a cost, and the ledger still balances because both readings subtract the same amount. The 5% line is a heuristic, and we'd rather say so. Tips are the exception: only the known Jito tip accounts count as a cost, and any other tip stays a plain "sent to" line. No guessing.

Round trips. A buy and a sell in one transaction (arbitrage, wash trades) can leave a wallet with a small net change and a long list of movements. Netting them would show a trade that never happened. If the asset you gave also came back inside the swap calls, or a route hop ran in the opposite direction, we refuse to write "Swapped A for B". The receipt shows both legs as they happened ("received X, paid back Y"), never netted.

Temporary WSOL accounts and rent. Swaps often open a wrapped-SOL token account, fund it, trade, and close it in the same transaction. Its rent deposit leaves and returns, so counting it as a cost is wrong, and ignoring wrapped accounts makes SOL vanish. We treat your wrapped accounts as part of your SOL, footnote temporary ones as "opened and closed, not counted", and refuse if a wrapped account ends at a balance we can't explain. The opposite case matters more. An emptied token account left open still holds rent. In the case-study swap that's 0.00151 SOL, about 3x what the swap paid out, and the receipt says so.

What it doesn't do yet

  • Mainnet only. Other clusters are refused.
  • Dollar amounts use today's SOL price, not the price at the time.
  • Pool-level fees aren't itemised. The case study decodes the Pump.fun pool fee for that one swap. The live tool shows what the balances show and doesn't split each pool's fee yet.
  • One wallet's point of view, the fee payer's. If a second signer also moved funds, you get the reduced receipt.
  • A short list of programs. Lending, staking, perps and NFT marketplaces get the reduced receipt for now.
  • Older transactions whose token balances don't record owners are listed by token account, not wallet.
  • It depends on two free public RPCs. If neither has your transaction, or they rate-limit you, you get an error, not a guess.
  • The verification harness and fixtures aren't in the public repo yet. The repo has the app only.

Try it

Open the tool and paste a signature or a solscan.io / explorer.solana.com link, or use the three examples under the box: a SOL swap through three pools, a 289.7 USDC transfer to a new account, and a swap that failed on slippage. The code is MIT: github.com/swiftbotlab/solscan-receipt-redesign.

If a transaction comes back reduced and you think we should be able to explain it, or a figure looks off, open an issue with the signature. Those are the most useful bug reports we can get.

If you want the same receipt from a program or an agent, the engine also runs as a small pay-per-call API: GET https://x402-receipt.swiftbotlab.workers.dev/receipt?sig=<signature> returns the receipt as JSON. It uses x402: the first response is a 402 with the terms (USDC on Base, $0.02 per call), and failed lookups are never charged. Docs and an OpenAPI description are at the root URL.

We're Swift Bot Labs, a small studio building bots and developer tools. If you want a plain-language view like this in your own product, the intake is at t.me/SwiftBotLabsBot?start=quote.

Top comments (0)