Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions benchmarks/terminal-fill-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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"}
Expand Down
15 changes: 15 additions & 0 deletions harnesses/terminal-fill-quality/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions harnesses/terminal-fill-quality/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
67 changes: 66 additions & 1 deletion harnesses/terminal-fill-quality/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, ""
}
1 change: 1 addition & 0 deletions harnesses/terminal-fill-quality/cmd/script/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions harnesses/terminal-fill-quality/cmd/script/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 7 additions & 4 deletions harnesses/terminal-fill-quality/cmd/script/ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Loading