Introduction
What Flow Trades is, how it works, and why you'd use it.
What is Flow Trades?
Flow Trades is a self-hosted Solana swap API. You run a single binary on your own server, point it at a Geyser node, and it gives you a REST API for quoting and executing token swaps across 19 DEXes — Raydium, Orca, Meteora, PumpFun, and more.
You run it on your own infrastructure, with your own fee collection, fully self-hosted.
How do users interact with it?
The typical flow is two API calls:
- Quote —
GET /quotewith an input token, output token, and amount. Flow finds the best route across all pools and returns the expected output, price impact, and route details. - Swap —
POST /swapwith the quote and the user's wallet address. Flow builds an unsigned Solana transaction wrapped in the on-chain router (for fee collection) and returns it as base64. The user signs it with their wallet and submits it to the network.
That's it. Your frontend or bot calls /quote, shows the user what they'll get, then calls /swap, has them sign, and submits. Flow never holds keys or funds — it only builds transactions.
How does fee collection work?
Every swap transaction is wrapped in a CPI to the Flow router program (FLoWxx...UsNqm), an immutable Solana program deployed on mainnet. The router takes a 0.5% fee from the output token after the swap completes and the slippage check passes. 70% of the fee goes to the integrator (you), 30% to the protocol. Fees accumulate in token accounts automatically — SOL swaps collect SOL fees, USDC swaps collect USDC fees, etc.
The router program's upgrade authority has been permanently burned. Nobody can change the fee rate, treasury wallet, or program logic.
What do I need to run it?
- A Yellowstone Geyser gRPC endpoint — either a local validator with the Geyser plugin, or a hosted provider
- A Solana RPC endpoint — for initial pool data and optional simulation
- That's it. No databases, no Docker, no dependencies. One binary and a config file.
Quick Start
Get Flow Trades running in under 5 minutes.
0. Download
Grab the latest binary from GitHub Releases.
chmod +x flow-trades
1. Configure
# config.toml
rpc_url = "https://your-solana-rpc.com"
[streaming]
geyser_endpoint = "http://your-geyser-node:10000"
2. Run
./flow-trades
Server starts on http://127.0.0.1:8080. Pools discovered automatically from Geyser, persisted to SQLite.
3. Quote
curl "http://localhost:8080/quote?\
input=So11111111111111111111111111111111111111112&\
output=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&\
amount=1000000000"
4. Swap
QUOTE=$(curl -s "http://localhost:8080/quote?input=So111...&output=EPjFW...&amount=1000000000")
curl -X POST http://localhost:8080/swap \
-H "Content-Type: application/json" \
-d "{\"wallet\": \"YOUR_PUBKEY\", \"quote\": $QUOTE}"
Returns a base64-encoded unsigned VersionedTransaction. Sign with your wallet and submit via sendTransaction.
Configuration
| Option | Env Var | Default | Description |
|---|---|---|---|
rpc_url | RPC_URL | required | Solana RPC endpoint |
geyser_endpoint | GEYSER_ENDPOINT | -- | Yellowstone gRPC endpoint |
geyser_token | GEYSER_TOKEN | -- | gRPC auth token (if required) |
listen | LISTEN_ADDR | 127.0.0.1:8080 | API listen address |
referral_account | REFERRAL_ACCOUNT | -- | Your wallet for 70% fee share |
pool_db | POOL_DB_PATH | ./pools.db | SQLite pool database path |
alt_addresses | ALT_ADDRESSES | -- | Address Lookup Tables (comma-separated) |
swap_stream_enabled | SWAP_STREAM_ENABLED | true | Enable /swap-stream (broadcast hub + parser + SOL oracle) |
swap_stream_buffer_size | SWAP_STREAM_BUFFER_SIZE | 8192 | Per-subscriber lossy broadcast capacity |
sol_price_refresh_secs | SOL_PRICE_REFRESH_SECS | 10 | SOL/USD oracle refresh cadence |
log_level | LOG_LEVEL | warn | trace, debug, info, warn, error |
Requirements
- Yellowstone Geyser gRPC endpoint (local validator or hosted provider)
- Solana RPC endpoint (for companion data + simulation)
- 512MB RAM minimum, 1GB recommended
- No external databases or dependencies — single binary
API Reference
GET /quote
Find the best swap route for a token pair.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
input | string | Yes | -- | Input token mint (base58) |
output | string | Yes | -- | Output token mint (base58) |
amount | string | Yes | -- | Raw amount in smallest units |
slippage | u16 | No | 50 | Slippage in basis points (50 = 0.5%) |
direct_only | bool | No | true | false enables multi-hop routing |
dexes | string | No | all | Whitelist DEXes (comma-separated) |
exclude | string | No | none | Blacklist DEXes (comma-separated) |
mode | string | No | ExactIn | ExactIn or ExactOut |
Response
{
"input_token": "So11111111111111111111111111111111111111112",
"amount_in": "1000000000",
"output_token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount_out": "162340000",
"minimum_out": "161528300",
"mode": "ExactIn",
"slippage_bps": 50,
"price_impact": "0.12",
"routes": [
{
"pool": {
"pool_address": "...",
"dex": "Orca",
"input_token": "So111...",
"output_token": "EPjFW...",
"amount_in": "1000000000",
"amount_out": "162340000",
"fee": "2500000",
"fee_token": "So111..."
},
"percent": 100
}
],
"platform_fee": {
"amount": "811700",
"fee_bps": 50,
"fee_token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"side": "output"
},
"slot": 412875000,
"quote_time_ms": 0.04
}
POST /swap
Build an unsigned versioned transaction from a quote. All swaps routed through the on-chain router.
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
wallet | string | Yes | -- | Signer public key (base58) |
quote | object | Yes | -- | Full response from GET /quote |
priority_fee | u64 | No | 5000 | Priority fee in lamports |
compute_limit | u32 | No | auto | Compute unit limit |
simulate | bool | No | false | Simulate first (returns CU + logs) |
tip | object | No | -- | { "address": "...", "lamports": 10000 } |
Response
{
"transaction": "base64-encoded-unsigned-VersionedTransaction...",
"block_height": 412875100,
"compute_limit": 400000,
"priority_fee": 5000
}
GET /health
Server status, pool counts, stream stats, blockhash age.
{
"status": "ok",
"poolCacheSize": 1333,
"registrySize": 5787,
"poolDbSize": 5787,
"streamUpdates": 19754,
"streamErrors": 0,
"blockhashCacheAgeMs": 200,
"lastStreamUpdateMs": 3
}
GET /metrics
Prometheus exposition format. Counters for quotes, stream updates, errors. Gauges for cache/registry/SQLite sizes.
WS /quote-ws
Push-based quote streaming. Send a subscription, receive quote updates at your interval.
Subscription Message
{
"input_mint": "So11111111111111111111111111111111111111112",
"output_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": 1000000000,
"slippage_bps": 50,
"interval_ms": 1000
}
Minimum interval: 100ms. Server pushes full QuoteResponse objects at the specified rate.
WS /swap-stream
Live broadcast of every confirmed DEX swap parsed off the block stream, enriched with native price (output ui per input ui + inverted) and USD price (per output token + total trade size).
Distinct from /quote-ws — that streams projected prices for a chosen pair; this streams observed swaps as they confirm on-chain.
Protocol
- Connect to
ws://your-host:8080/swap-stream. - (Optional) Send a JSON filter as the first frame. Skipping defaults to "match all" after a 5 s grace.
- Server replies once with
{"type":"subscribed"}, then streams{"type":"swap", …}JSON frames. - Server emits
{"type":"ping"}every 30 s as a keep-alive. - Slow consumers see
{"type":"lagged","skipped":N}instead of being disconnected — the connection stays open. (Backed bytokio::sync::broadcast— lossy by design so a slow client never blocks the parsing pipeline.)
Subscribe Filter
{
"type": "subscribe",
"filter": {
"dex": ["Raydium V4", "Pumpup Bonding"],
"mint": "9U3FcH1Z3vZFHvN5KrkHHkuJSPKKnBBLpPQ1FkezxAai",
"pool": "AQxKPt88jGP1DiwRbqweoo74Yi2o3fMTATAbDDA6BVLT",
"min_amount_usd": 10.0
}
}
All filter fields optional and AND'd together. dex matches any of the listed labels (see Supported DEXes). mint matches a swap's input or output. min_amount_usd drops swaps whose amount_usd is null or below the threshold.
Wire Format
{
"type": "swap",
"signature": "5xY...",
"slot": 412341234,
"block_time": 1745000000,
"dex": "Pumpup Bonding",
"pool": "AQxKPt88jGP1DiwRbqweoo74Yi2o3fMTATAbDDA6BVLT",
"user": "7AakHWVQ42d1FyaG7pqvE9ArmhWUpcpvd4yhKKd2dBnt",
"input": { "mint": "So11...", "amount": "1000000", "decimals": 9, "ui_amount": "0.001" },
"output": { "mint": "9U3F...", "amount": "24637903819", "decimals": 6, "ui_amount": "24637.9" },
"price_native": "24637903.82",
"price_native_inverted": "0.0000000406",
"price_usd": "0.00000647",
"amount_usd": "0.16"
}
| Field | Description |
|---|---|
signature | Solana transaction signature (base58) |
slot | Slot the swap landed in |
block_time | Unix epoch seconds (RPC-provided; nullable) |
dex | Human-readable DEX label (matches /program-id-to-label) |
pool | Pool address (or pool_sol_account for bonding curves) |
user | Fee payer of the underlying tx — the user who did the swap |
input.amount | Atomic units, decimal string (avoids JSON-number precision loss) |
input.ui_amount | amount / 10^decimals formatted as a decimal string |
price_native | Output ui per input ui (output amount the user got, divided by what they paid) |
price_native_inverted | Input ui per output ui |
price_usd | USD per output token (one-sided). null when neither side is a quote mint. |
amount_usd | Total trade size in USD. null when neither side is a quote mint. |
USD Enrichment
- USDC, USDT, PYUSD are treated as $1.00.
- SOL is priced via the in-process oracle: Binance public ticker (primary), DexScreener (fallback). Refreshed at startup and then every
--sol-price-refresh-secs(default 10 s). - When neither side is SOL nor a stable mint,
price_usdandamount_usdarenull. No external lookup is attempted, by design (zero deps, self-hosted).
Node.js Example
import WebSocket from "ws";
const ws = new WebSocket("ws://localhost:8080/swap-stream");
ws.on("open", () => ws.send(JSON.stringify({
type: "subscribe",
filter: { min_amount_usd: 10 }
})));
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (msg.type === "swap") {
console.log(
`${msg.dex} ${msg.input.ui_amount} ${msg.input.mint.slice(0,4)}... ` +
`-> ${msg.output.ui_amount} ${msg.output.mint.slice(0,4)}... ` +
`$${msg.amount_usd}`
);
} else if (msg.type === "ping") {
ws.send(JSON.stringify({ type: "pong" }));
} else if (msg.type === "lagged") {
console.warn(`stream lagged: ${msg.skipped} swaps dropped for slow consumer`);
}
});
Disabling
Set --swap-stream-enabled false (or SWAP_STREAM_ENABLED=false) to disable the broadcast hub, parser, and SOL/USD oracle entirely. Saves a small amount of memory and the periodic background HTTP fetch.
Verified throughput
A 20-minute mainnet soak streamed 340,613 swaps at 283.84 swaps/sec sustained with 99.1% USD coverage, zero broadcast lag, zero reconnects, and zero consumer errors. Server stayed at 16.6% CPU and ~300 MB RSS with no leak.
Fee Structure
| Property | Value |
|---|---|
| Router Program | FLoWxxKoBrZtNj5NTPuy1tZcSU6Nnjtz7v5snrrUsNqm |
| Platform Fee | 0.5% (50 bps) on output token |
| Integrator Share | 70% of fee |
| Protocol Share | 30% of fee |
| Upgrade Authority | none (immutable) |
Fees are always taken from the output token after the swap completes and slippage is verified. Collected in any SPL token — SOL, USDC, memecoins, Token-2022.
Treasury + referral ATAs are auto-created on first swap per output mint (~0.002 SOL rent, paid by signer once).
On-Chain Router
Every POST /swap transaction is wrapped in the flow-router CPI. The router program is immutable — upgrade authority has been permanently burned. No one can change the fee rate, treasury, or program logic.
How It Works
User TX -> flow-router::swap
|-- Read config PDA (fee, treasury, split)
|-- Validate token program (SPL v1 or Token-2022)
|-- Execute N sequential DEX CPIs (1-5 hops)
|-- Verify final output >= min_amount_out
|-- Validate fee account owned by treasury
'-- Collect fee from output token
Integrators
Earn 70% of the 0.5% platform fee on every swap routed through your integration.
- Contact the admin to get your wallet whitelisted
- Set
referral_accountin yourconfig.toml - Fees accumulate in your wallet's ATAs automatically
Supported DEXes (21 pool types)
| # | DEX | Type |
|---|---|---|
| 1 | Raydium V4 | Legacy AMM |
| 2 | Raydium CPMM | Constant Product |
| 3 | Raydium CLMM | Concentrated Liquidity |
| 4 | Raydium LP | StableSwap |
| 5 | PumpFun | Bonding Curve |
| 6 | PumpFun AMM | Graduated AMM |
| 7 | Orca Whirlpool | Concentrated Liquidity |
| 8 | Meteora Standard | Constant Product |
| 9 | Meteora DLMM | Dynamic LMM |
| 10 | Meteora DAMM v2 | Dynamic AMM |
| 11 | Meteora DBC | Bonding Curve |
| 12 | FluxBeam | SPL Token Swap |
| 13 | DefiTuna Fusion | CLMM |
| 14 | DefiTuna Pools | Position Manager |
| 15 | Saros | SPL Token Swap |
| 16 | Dooar | SPL Token Swap |
| 17 | PancakeSwap | CLMM |
| 18 | FlashTrade | Custom |
| 19 | Byreal | CLMM |
| 20 | Pumpup AMM | Constant Product (post-graduation) |
| 21 | Pumpup Bonding | Bonding Curve (pre-graduation, native SOL) |
Plus OnChain Labs DEX V2 (proVF4...) — aggregator router into 80+ private MM venues. Discovery-only: pool addresses behind it are registered against their underlying DEXes by the block scanner; we do not quote or execute against the aggregator program itself.
Performance
| Metric | Value |
|---|---|
| Quote latency | 0-block latency |
| Quote p99 (1000 iterations) | 47µs |
| Concurrent throughput | 29,359 quotes/sec |
| Full pipeline (quote -> IX -> TX) | 816µs avg |
| Pool discovery rate | ~500 pools/min |
| RPC calls (steady state) | 0 |
| CPU | <1% |
| Memory | ~35MB |
Error Codes
Router Errors (on-chain)
| Code | Name | Description |
|---|---|---|
| 0 | InvalidInstructionData | Bad instruction format |
| 1 | SlippageExceeded | Output < min_amount_out |
| 2 | InvalidFeeBps | Fee > 10000 bps |
| 3 | BalanceReadFailed | Token account unreadable |
| 5 | NotEnoughAccounts | Missing required accounts |
| 6 | InvalidAccount | Wrong owner or discriminator |
| 7 | AlreadyInitialized | Config/integrator PDA exists |
| 8 | Unauthorized | Not admin or not whitelisted |
API Errors
| Status | When |
|---|---|
| 400 | Invalid parameters (bad mint, zero amount, same input/output) |
| 404 | No route found for the given pair |
| 500 | Router not configured, RPC failure, or internal error |
Security
The Flow router smart contract has been audited by SigIntZero.
- Zero
unwrap(),expect(), orpanic!()in production code - 35 explicit validation checks across all instructions
- Upgrade authority permanently burned — program is immutable
- Config PDA is write-once — fee rate, treasury, and split cannot be changed
- All fee account ownership validated against treasury wallet on every swap
- Token program explicitly validated (SPL Token v1 or Token-2022 only)