diff --git a/benchmarks/terminal-fill-quality.yml b/benchmarks/terminal-fill-quality.yml index 1ab879624..b612ec514 100644 --- a/benchmarks/terminal-fill-quality.yml +++ b/benchmarks/terminal-fill-quality.yml @@ -78,7 +78,7 @@ methodology: - "Phantom (Solana): its in-wallet swap pays 0.85 % to its fee wallet in SOL or WSOL, sampled like the other Solana terminals. Binance Wallet (BSC, Ethereum, Base): one router on the three chains, no event of its own, so its swaps are the transactions sent to it in blocks read in full; valued like the other native EVM rows. Terminal's EVM fee wallets get under $10 a day: not measured. Banana Gun on Ethereum: buys only, no fee, its users sell on Uniswap directly; held out (one side)." - "four.meme's bonding curve on BNB is priced exactly: the curve is a constant product on virtual reserves (checked on consecutive trades to 1e-12), its constants come from the TokenManager's token info, so the mid before the trade is known; the launchpad's 1 % fee is 'other', like pump.fun's on Solana." - "Kept out of that residual: a fee the pool's own hook keeps (v4 launchpad pools on Robinhood Chain 1 %, BNB 3.8 %, paid whatever the router) is a pool cost; a native leg (BNB sent, ETH received) is valued at the mid of the route's own stable-to-gas-coin hop before that hop, so the hop's LP fee and impact stay a pool cost; Axiom's inclusion tip (0.0025 BNB fixed; on Robinhood Chain the user's setting, from the call trace) is network cost, like the Solana tip relays." - - "Not in the table: BullX (suspended June 2026), Nova (no live fee wallet), Bloom's and Photon's EVM swappers (routers unknown), BasedBot on Solana (fee wallet unknown). BasedBot: its Relay requests are its in-app bridge between the user's Solana and EVM wallets, both ways, no app fee (a bridge leg in the JSON, not a trade); its rows are those wallets' swaps on its router on Robinhood Chain, BNB, Base and Ethereum. GMGN's other routers are unverified. HyperEVM: funding legs only so far, no tab." + - "Not in the table: BullX (suspended June 2026), Nova (no live fee wallet), Bloom's and Photon's EVM swappers (routers unknown). BasedBot's Relay requests are its in-app bridge between the user's Solana and EVM wallets, both ways, no app fee (a bridge leg, not a trade); its EVM rows are those wallets' swaps on its router. On Solana its fee is a separate transaction after the swap, so failed swaps are not seen: no fail rate there. HyperEVM rows (pump.fun app, Phantom via Relay): no tab yet." findings: - "{{best_name}} fills cheapest: a median {{best_p50}} lost per swap against the pool's state before the trade, all costs included." @@ -369,8 +369,8 @@ providers: - slug: basedbot name: BasedBot - tag: "Telegram bot, Robinhood Chain + BNB + Base + Ethereum (wallets funded from Solana through Relay)" - formula: "Swaps of the wallets on BasedBot's Relay legs (either end), on its router on Robinhood Chain, BNB, Base and Ethereum, valued at the pool's state before the swap; its fee and referral transfers (about 1 %) are the fee. Median over 24 h." + tag: "Telegram bot, Solana + Robinhood Chain + BNB + Base + Ethereum" + formula: "Solana: swaps whose 1 % fee arrives in a separate transaction to BasedBot's wallet (the payer's previous transaction). EVM: the wallets on its Relay legs, on its router. Valued at the pool's state before the swap; median over 24 h." queries: p50: tfq_loss_bps{terminal="basedbot",chain="all",stat="median"} p90: tfq_loss_bps{terminal="basedbot",chain="all",stat="p90"} diff --git a/harnesses/terminal-fill-quality/README.md b/harnesses/terminal-fill-quality/README.md index e1449d672..f3b4079a3 100644 --- a/harnesses/terminal-fill-quality/README.md +++ b/harnesses/terminal-fill-quality/README.md @@ -142,6 +142,21 @@ depends on pool activity. The victim's extra cost is already inside **Trade-size buckets**: `by_size` per terminal (under $25, $25 to $250, over $250; median loss and n from 5 samples). +**BasedBot on Solana (`FeeTx`)**: the swap runs through Jupiter (or another +router) with nothing inside; right after, the user signs a second transaction +with 1 % of the SOL spent (buys) or received (sells), a ~$0.10 minimum, +split between BasedBot's wallet `HR6xhpJY…` (12 to 85 %, ~2,600 +transactions an hour on 2026-09-21) and per-user or referrer accounts. +Found with a test trade of Florent's (`2Z375icD…` then `3s9JYYT6…`) and +checked on 12 other payers (12/12: the payer's previous transaction is the +swap, fee / spent = 1.00 % above the minimum). The WebSocket feed counts a +successful mention of the wallet with no swap program as an attempt; the +sampler reads the fee transaction, walks one signature back on the payer +(`getSignaturesForAddress` with `before`), parses that swap, then adds the +fee transaction's SOL out as terminal fee and its transaction fee as +network (`feeTxSwap`). Failed swaps leave no fee transaction: the row has +no fail rate. `fee_sig` in the JSON names the fee transaction. + **Launchpad curves (`rhcurve.go`)**: BasedBot's router family also trades bonding curves, one contract per token with the same bytecode, no Uniswap event: `buy 0xec36bf57…` [gas coin in, tokens out, fee 1, fee 2], `sell diff --git a/harnesses/terminal-fill-quality/cmd/script/config.go b/harnesses/terminal-fill-quality/cmd/script/config.go index 1193c0189..167ed789a 100644 --- a/harnesses/terminal-fill-quality/cmd/script/config.go +++ b/harnesses/terminal-fill-quality/cmd/script/config.go @@ -29,6 +29,13 @@ type Terminal struct { Internal []string `json:"internal,omitempty"` Tips []string `json:"tips,omitempty"` Programs []string `json:"programs,omitempty"` + // FeeTx: the terminal's fee is a separate transaction the user signs + // right after the swap (BasedBot on Solana: the swap runs through + // Jupiter with nothing inside, then 1 % of the SOL spent or received, + // a ~$0.10 minimum, goes out in system transfers to the wallet and to + // per-user or referrer accounts). A mention of the wallet with no swap + // program is the attempt; the payer's previous transaction is the swap. + FeeTx bool `json:"fee_tx,omitempty"` // StableLegsAreFee: besides its main fee wallet, the terminal's fee // arrives as user-signed stable transfers to per-trade accounts (FOMO // through the OKX router: commission leg plus one or two transferChecked @@ -123,6 +130,14 @@ var terminals = []Terminal{ // same transaction (DeFiLlama's pumpfun-app adapter attributes on it). // The fixed 0.001 SOL it forwards to its pfn… accounts on every swap is // an inclusion tip (network), like the other terminals' relays. + // BasedBot on Solana (2026-09-21, a test trade of Florent's and 12 other + // payers): Jupiter swap with no fee inside, then a separate transaction + // with 1 % of the SOL spent (buys) or received (sells), a ~$0.10 minimum, + // split between this wallet (12 to 85 %, ~2,600 transactions an hour) and + // per-user or referrer accounts. Failed swaps leave no fee transaction: + // the fail rate is not measured on this row. + {Slug: "basedbot", Name: "BasedBot", Kind: "bot", Wallets: []string{"HR6xhpJYYiTvmAtUZvpLMN7qbr6TxCw1Sgq3fab8vCoa"}, FeeTx: true, + Note: "BasedBot's Solana swap runs through Jupiter with no fee inside; its 1 % fee (about $0.10 minimum) is a separate transaction right after, to its wallet and per-user accounts: the whole of it is terminal fee, its transaction fee is network. The fee is charged after a successful swap only, so failed swaps are not seen: no fail rate on this row."}, {Slug: "pump-fun", Name: "pump.fun app", Kind: "app", Programs: []string{"6Vo3245eszAb5wuqEMw8mGdbfRUdKbHhDHP5LcaGuTAB"}, Note: "pump.fun's mobile app takes no fee of its own; the fixed 0.001 SOL it forwards per swap to its pfn… accounts is counted as network, and pump.fun's protocol and creator fees sit in other."}, // Not in the cohort: BullX (trading suspended 2026-06-01; its wallets diff --git a/harnesses/terminal-fill-quality/cmd/script/main.go b/harnesses/terminal-fill-quality/cmd/script/main.go index 5983da68d..2d1038df2 100644 --- a/harnesses/terminal-fill-quality/cmd/script/main.go +++ b/harnesses/terminal-fill-quality/cmd/script/main.go @@ -377,6 +377,12 @@ func main() { wsURL = "wss://api.mainnet-beta.solana.com" } fd = newFeed(wsURL) + fd.feeTx = map[string]bool{} + for _, t := range terminals { + if t.FeeTx { + fd.feeTx[t.Slug] = true + } + } go fd.run(context.Background()) } if st.EvmCursor == nil { @@ -588,7 +594,13 @@ func sample(ctx context.Context, rpc *rpcClient, st *State, solUSD float64, pool st.reject(t.Slug, "unreadable") continue } - sw, reject := parseSwap(t, s.Signature, tx, solUSD, "") + var sw *Swap + var reject parseReject + if t.FeeTx { + sw, tx, reject = feeTxSwap(ctx, rpc, t, s.Signature, tx, solUSD) + } else { + sw, reject = parseSwap(t, s.Signature, tx, solUSD, "") + } if reject != "" { st.reject(t.Slug, reject) continue @@ -2287,3 +2299,56 @@ func loopersOf(st *State, slugs []string) map[string]bool { } return loopers } + +// feeTxSwap: a terminal whose fee is a separate transaction (BasedBot on +// Solana). feeTx is the sampled fee transaction; the swap is the payer's +// previous transaction. The swap is parsed as usual (the fee wallet is +// not in it, so its terminal fee reads 0), then the fee transaction's SOL +// out (every lamport that left the payer, less the transaction fee) is +// added as terminal fee, its transaction fee as network, both to what the +// user gave. Returns the swap's transaction for the pricing step. +func feeTxSwap(ctx context.Context, rpc *rpcClient, t Terminal, feeSig string, feeTx *parsedTx, solUSD float64) (*Swap, *parsedTx, parseReject) { + msg := feeTx.Transaction.Message + if len(msg.AccountKeys) == 0 || len(feeTx.Meta.PreBalances) != len(msg.AccountKeys) { + return nil, nil, rejectNotSwap + } + payer := msg.AccountKeys[0].Pubkey + feeOut := float64(int64(feeTx.Meta.PreBalances[0])-int64(feeTx.Meta.PostBalances[0])) / 1e9 + txFee := float64(feeTx.Meta.Fee) / 1e9 + feeOut -= txFee + if feeOut <= 0 { + return nil, nil, rejectNotSwap + } + prev, err := rpc.signaturesBefore(ctx, payer, feeSig, 1) + if err != nil || len(prev) == 0 { + return nil, nil, "no_swap_before_fee" + } + if prev[0].failed() { + return nil, nil, "swap_before_fee_failed" + } + if prev[0].BlockTime != nil && feeTx.BlockTime != nil && *feeTx.BlockTime-*prev[0].BlockTime > 300 { + return nil, nil, "no_swap_before_fee" // the fee transaction did not follow a swap within five minutes + } + swapTx, err := rpc.transaction(ctx, prev[0].Signature) + if err != nil || swapTx == nil { + return nil, nil, "unreadable" + } + sw, reject := parseSwap(t, prev[0].Signature, swapTx, solUSD, payer) + if reject != "" { + return nil, nil, reject + } + // The fee transaction's amounts in the swap's quote unit. + q := 1.0 + if sw.Quote != "SOL" && sw.QuoteUSD > 0 { + q = solUSD / sw.QuoteUSD + } + sw.TerminalQ += feeOut * q + sw.NetworkQ += txFee * q + if sw.Side == "buy" { + sw.UserQ += (feeOut + txFee) * q + } else { + sw.UserQ -= (feeOut + txFee) * q // received net of the fee paid right after + } + sw.FeeSig = feeSig + return sw, swapTx, "" +} diff --git a/harnesses/terminal-fill-quality/cmd/script/parse.go b/harnesses/terminal-fill-quality/cmd/script/parse.go index 417a85468..2ea17b910 100644 --- a/harnesses/terminal-fill-quality/cmd/script/parse.go +++ b/harnesses/terminal-fill-quality/cmd/script/parse.go @@ -79,6 +79,7 @@ type Swap struct { // NetworkQ the origin gas. Chain string `json:"chain,omitempty"` RelayQ float64 `json:"relay_q,omitempty"` + FeeSig string `json:"fee_sig,omitempty"` // the separate fee transaction (BasedBot on Solana) RelayID string `json:"relay_id,omitempty"` InTx string `json:"in_tx,omitempty"` QuoteUSD float64 `json:"quote_usd"` // quote unit price used for sizing diff --git a/harnesses/terminal-fill-quality/cmd/script/rpc.go b/harnesses/terminal-fill-quality/cmd/script/rpc.go index 17642222d..45a48ad4c 100644 --- a/harnesses/terminal-fill-quality/cmd/script/rpc.go +++ b/harnesses/terminal-fill-quality/cmd/script/rpc.go @@ -147,6 +147,13 @@ func (c *rpcClient) signatures(ctx context.Context, address string, limit int, u return out, err } +// signaturesBefore: the address's signatures older than `before`, newest first. +func (c *rpcClient) signaturesBefore(ctx context.Context, address, before string, limit int) ([]sigInfo, error) { + var out []sigInfo + err := c.call(ctx, "getSignaturesForAddress", []any{address, map[string]any{"limit": limit, "before": before, "commitment": "confirmed"}}, &out) + return out, err +} + // instruction in a jsonParsed transaction: program-owned instructions // carry their account list (pubkeys); the ones the RPC decodes (system, // spl-token, associated-token…) carry `parsed` instead. diff --git a/harnesses/terminal-fill-quality/cmd/script/ws.go b/harnesses/terminal-fill-quality/cmd/script/ws.go index 8c304c4cd..fca791509 100644 --- a/harnesses/terminal-fill-quality/cmd/script/ws.go +++ b/harnesses/terminal-fill-quality/cmd/script/ws.go @@ -35,9 +35,12 @@ type feed struct { reqs map[int64]string // request id -> terminal slug (until confirmed) box map[string]*inbox ok map[string]int // successful attempts of the last drained tick - up bool - last time.Time - conn *websocket.Conn // the live session, closed by resubscribe + // feeTx: the terminals whose fee is a separate transaction (no swap + // program in the mention): every successful mention is an attempt. + feeTx map[string]bool + up bool + last time.Time + conn *websocket.Conn // the live session, closed by resubscribe } // resubscribe closes the live session so the next one subscribes to the @@ -226,7 +229,7 @@ func (f *feed) handle(data []byte) { return } b.sigs[v.Signature] = true - if !isSwapAttempt(v.Logs) { + if !isSwapAttempt(v.Logs) && !(f.feeTx[slug] && !(sigInfo{Err: v.Err}).failed()) { b.other++ return }