Introduction
flow-trades is a self-hosted Solana swap API. One Rust binary: it discovers pools, prices them with each venue's own arithmetic, and returns unsigned transactions that settle through the on-chain flow-router. It never holds keys or funds.
GET /quote— best route and expected outputPOST /swap— unsigned transaction (v0 or v1) for the user to signWS /swap-stream— every confirmed DEX swap, with prices
Source: flow-trades-solana (server) and flow-router-solana (on-chain router). Both MIT.
Quick Start
git clone https://github.com/sigint-zero/flow-trades-solana.git
cd flow-trades-solana
cargo build --release
# config.toml
rpc_url = "https://your-solana-rpc.com"
[streaming]
geyser_endpoint = "http://your-geyser-node:10000" # optional
./target/release/flow-trades
curl "http://localhost:8080/quote?input=So11111111111111111111111111111111111111112&output=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=1000000000"
Without a Geyser endpoint the server streams blocks over RPC blockSubscribe; same features, slightly higher latency.
Configuration
| Option | Env | Default | Purpose |
|---|---|---|---|
rpc_url | RPC_URL | required | Solana RPC |
geyser_endpoint | GEYSER_ENDPOINT | -- | Yellowstone gRPC, optional |
geyser_token | GEYSER_TOKEN | -- | gRPC auth token |
listen | LISTEN_ADDR | 127.0.0.1:8080 | API address |
router_program_id | ROUTER_PROGRAM_ID | first-generation router | Router to wrap swaps with |
referral_account | REFERRAL_ACCOUNT | -- | Integrator wallet for the referral fee share |
alt_addresses | ALT_ADDRESSES | -- | Address lookup tables for v0 transactions |
pool_db | POOL_DB_PATH | ./pools.db | SQLite pool registry |
swap_stream_enabled | SWAP_STREAM_ENABLED | true | /swap-stream on/off |
log_level | LOG_LEVEL | warn | trace … error |
Priority: CLI flags > env vars > config.toml > defaults.
Requirements
- Solana RPC endpoint
- Yellowstone Geyser gRPC endpoint, optional
- 1 GB RAM recommended; Rust toolchain to build
API Reference
GET /quote
| Parameter | Required | Default | Description |
|---|---|---|---|
input | yes | -- | input mint |
output | yes | -- | output mint |
amount | yes | -- | raw amount, smallest units |
slippage | no | 50 | basis points |
direct_only | no | true | false allows 2- and 3-hop routes |
dexes / exclude | no | -- | DEX label allow / deny list |
{
"input_token": "So111…", "output_token": "EPjFW…",
"amount_in": "1000000000", "amount_out": "162340000", "minimum_out": "161528300",
"slippage_bps": 50, "price_impact": "0.12",
"routes": [ { "pool": { "pool_address": "…", "dex": "Orca", "amount_in": "1000000000", "amount_out": "162340000", "fee": "40000" }, "percent": 100 } ],
"platform_fee": { "amount": "0", "fee_bps": 0, "fee_token": "EPjFW…", "side": "output" },
"quote_time_ms": 0.05
}
amount_out is the DEX output before the router fee. On multi-hop routes each later hop is quoted on the previous hop's guaranteed output, so the quote is what the route delivers.
POST /swap
| Field | Required | Default | Description |
|---|---|---|---|
wallet | yes | -- | signer public key |
quote | yes | -- | the /quote response |
tx_version | no | 0 | 1 = transaction format v1 (4,096 B, no lookup tables); use when a v0 build is rejected as too large |
simulate | no | false | simulate and return CU + logs |
priority_fee | no | 5000 | lamports |
compute_limit | no | auto | compute units |
tip | no | -- | { "address": "…", "lamports": 10000 } |
{ "transaction": "base64…", "tx_version": 0, "block_height": 412875100, "compute_limit": 400000, "priority_fee": 5000 }
Sign the transaction with the wallet and submit it with sendTransaction.
GET /health
{ "status": "ok", "poolCacheSize": 2352, "registrySize": 25031, "streamUpdates": 2919, "streamErrors": 0,
"blockhashCacheAgeMs": 398, "lastStreamUpdateMs": 65,
"router": { "programId": "…", "feeBps": 0, "layout": "TransferChecked" } }
GET /metrics
Prometheus format: quotes served, stream updates and errors, cache, registry and database sizes.
WS /quote-ws
{ "input_mint": "So111…", "output_mint": "EPjFW…", "amount": 1000000000, "slippage_bps": 50, "interval_ms": 1000 }
Server pushes a full quote at the interval (minimum 100 ms).
WS /swap-stream
Every confirmed DEX swap parsed from blocks, with native and USD prices. Optional first frame:
{ "type": "subscribe", "filter": { "dex": ["PumpFun AMM"], "mint": "…", "pool": "…", "min_amount_usd": 10.0 } }
{ "type": "swap", "signature": "…", "slot": 412341234, "dex": "PumpFun AMM", "pool": "…", "user": "…",
"input": { "mint": "So111…", "amount": "1000000", "decimals": 9, "ui_amount": "0.001" },
"output": { "mint": "…", "amount": "24637903819", "decimals": 6, "ui_amount": "24637.9" },
"price_native": "24637903.82", "price_usd": "0.00000647", "amount_usd": "0.16" }
Frames: subscribed once, swap, ping every 30 s, lagged when a slow consumer drops messages. USDC, USDT and PYUSD count as $1; SOL comes from a built-in oracle; other pairs have null USD fields.
Fee Structure
| Rate | fee_bps in the router's config account; 0 = no fee, no fee accounts |
| Side | output token, after the slippage check |
| Split | referral_split_bps to a whitelisted integrator, rest to the treasury |
The server reads the fee from the router named by ROUTER_PROGRAM_ID and reports it in every quote's platform_fee.
On-Chain Router
Source: flow-router-solana. One instruction runs 1–5 DEX legs as CPIs, checks the final output against min_amount_out, and collects the fee with TransferChecked if one is configured.
| Generation | Program | Fee | Notes |
|---|---|---|---|
| First | FLoWxxKoBrZtNj5NTPuy1tZcSU6Nnjtz7v5snrrUsNqm | 50 bps, 70/30 | immutable; plain Transfer, so Token-2022 mints with extensions cannot be traded through it; audited by SigIntZero |
| Second | built from flow-router-solana | configurable, including 0 | TransferChecked; upgradeable |
Integrators
If the router charges a fee, a whitelisted integrator wallet receives referral_split_bps of it. Set referral_account to that wallet; the admin whitelists it with add_integrator.
Supported DEXes
| Venue | Quoted |
|---|---|
| Raydium CPMM, Raydium CLMM, Raydium LaunchLab, Orca Whirlpool, PancakeSwap, DefiTuna Fusion, Meteora DAMM v2, Meteora Standard, Dooar | yes, exact venue math |
| pump.fun AMM | yes, exact curve and fee tiers |
| Saros | yes, fee observed from its swaps |
| Raydium V4, pump.fun bonding, Meteora DLMM, Meteora DBC, FluxBeam | streamed, not quoted |
| Byreal | not quotable (Raydium CLMM fork, parser uses the Orca layout) |
| FlashTrade, Pumpup AMM, Pumpup Bonding, DefiTuna Pools | stale |
| OnChain Labs DEX V2 | aggregator into private market makers; discovery only |
Performance
| Metric | Value |
|---|---|
/quote, warm, direct or multi-hop | 0.1–0.5 ms end to end |
/quote, first quote of an unseen pair | 0.5–2 s, then warm |
| Quote engine, 300 pools | 50 µs direct / 70 µs routed (p50) |
/swap build | 0.5–1.6 ms cached; 160–520 ms with RPC lookups |
| Quote vs simulated output | 0 bps on fresh state; ~5 bps multi-hop |
| Swap stream | ~110 swaps per slot |
| Memory | ~35 MB |
Error Codes
| Router | |
|---|---|
| 1 | SlippageExceeded |
| 2 | InvalidFeeBps |
| 5 | NotEnoughAccounts |
| 6 | InvalidAccount |
| 7 | AlreadyInitialized |
| 8 | Unauthorized |
| API | |
|---|---|
| 400 | bad parameters, or a v0 transaction over 1,232 bytes (retry with tx_version: 1) |
| 404 | no route |
| 500 | router not configured, RPC failure |
Security
- The server never holds keys;
/swapreturns unsigned transactions - Router: no panics, typed errors; write-once config; token program, output mint and fee-account owner validated; one slippage check on the final output
- First-generation router audited by SigIntZero; second generation tested natively against SPL Token and Token-2022
Source Code
- github.com/sigint-zero/flow-trades-solana — server.
cargo build --release,cargo test --lib - github.com/sigint-zero/flow-router-solana — router.
cargo test,cargo build-sbf