A trade data collector for crypto markets, with an embedded database and a REST API.
- Connects to exchange WebSocket streams via flowsurface-exchange
- Persists trades to DuckDB
- Serves data via REST API, as JSON or Arrow IPC stream formats
It's a self-contained, portable server, designed to run on a small VPS for individual use; not for production or shared use.
-
Get the latest release (
linux-x86_64orlinux-aarch64). -
Copy the config template:
# in the extracted directory cp config.example.toml config.toml -
Run:
./flowsurface-server
Or with a custom config path:
./flowsurface-server --config /path/to/config.toml
cargo build --release
cp config.example.toml config.toml
# run
cargo run --releaseBy default the server looks for config.toml in the project root when the
executable path contains /target/; otherwise it looks next
to the executable (deployed binary). Use --config /path/to/config.toml to
override.
If
config.tomldoesn't exist, the server writes the default template to that path and exits (code 2). Edit the created file to suit your needs, then re-run.
See config.example.toml for all
available options with inline documentation.
| Option | Default | Description |
|---|---|---|
bind_address |
127.0.0.1:8080 |
Listen address; non-loopback enables TLS + auth. |
max_requests |
500 |
Per-IP rate limit (req/10s); 0 = off. |
| Option | Default | Description |
|---|---|---|
data_dir |
"data" |
Directory for DB, auth token, and TLS certs. |
max_storage_mb |
4096 |
Hard cap on DB+WAL (MB); 0 = unlimited. |
data_retention_hours |
168 |
Purge trades older than this; 0 = keep all. |
memory_limit_mb |
0 |
DuckDB RAM cap; 0 = DuckDB default (80%). |
Low-memory hosts: By default DuckDB is allowed to use 80% of your machine's RAM. On a small host, this can leave too little for the rest of the system, and queries can fail with an
Out of Memory Error. For such cases, you better explicitly setmemory_limit_mbto a lower value, such as256or384for 1 GB host, so that DuckDB can spill to disk instead of failing when it's at the cap.
| Option | Default | Description |
|---|---|---|
base_assets |
[] |
Bases combined with whitelist quotes to determine tracked pairs. |
discovery_mode |
true |
Fetch metadata for all exchanges (populates /exchanges). |
The whitelist defines which pairs to track per venue and market kind.
See the [whitelist.*] sections in config.example.toml
for the template. The server combines base_assets × quote_assets
per market kind and resolves the correct exchange-specific ticker strings
(handling separators, _PERP, -SWAP suffixes, etc.).
Wildcard: An empty string
""as a quote asset includes every ticker on that exchange whose base matches (e.g.linear = [""]would track all linear perpetuals with a matching base asset regardless of quote currency).
Pairs are only tracked when both base_assets and a matching whitelist
entry are configured. If either is empty, no pairs are tracked.
Use the /exchanges endpoint to browse available symbols before
deciding which pairs to track.
When binding to a non-loopback address, the server:
- Generates a random 256-bit auth token on first boot and persists
it as
.auth_tokeninside the configureddata_dir(see[storage]). - The token's first 4 characters are printed at startup.
- Retrieve the full token at any time:
# data_dir = "data" cat data/.auth_token
The token is reused across restarts. To set a specific token set the AUTH_TOKEN environment variable
(via .env or the environment).
The server automatically enables HTTPS with a self-signed certificate
when binding to a non-loopback address (e.g. 0.0.0.0:8080).
- Set
bind_address = "0.0.0.0:8080"inconfig.toml - Start the server, it generates (inside the configured
data_dir, see[storage]):cert.pem+key.pem(self-signed TLS cert/key pair).auth_token(random 256-bit token, unless already set)
- Clients connect via
https://vps-ip:8080using the auth token. The cert is self-signed, usecurl -kor equivalent to accept it, or verify againstcert.pemdirectly.
Port reachability: Some cloud providers block inbound ports by default. Configure a firewall / security-group rule to allow traffic on your chosen port.
Set tls_domain to have the self-signed cert identify as your domain:
tls_domain = "data.mydomain.com"When binding to 0.0.0.0, verified TLS requires a domain name (no IP SAN
is added). When binding to a concrete IP, an IP SAN is added automatically.
The cert's SHA-256 fingerprint is logged at startup for pinning.
For local-only use, keep bind_address = "127.0.0.1:8080":
- Plain HTTP (no TLS overhead)
- No auth token required
curl http://127.0.0.1:8080/pairsworks directly
This server is not a tick-by-tick market data recorder and shouldn't be treated as one:
-
No delivery guarantees: WebSocket disconnections may cause gaps. Exchange-side replays may introduce duplicates. The server uses flowsurface-exchange for market feeds, which is built for charting rather than archival.
-
In-memory buffering: trades are held in memory for up to
flush_interval_ms(default 2s) before batch-writing to DuckDB. Under extreme load, if the buffer exceedsmax_buffered_trades, the oldest entries are shed to prevent OOM crashes. -
Configuration changes require a restart: editing tracked pairs or anything in
config.tomlneeds a server restart. In-memory buffered trades are flushed to disk during shutdown, but there will be a data gap until the server restarts and feeds reconnect.
It prioritizes convenience over guaranteed delivery, as it's simply made as a companion for flowsurface.
/status is public (no auth).
All other endpoints require Authorization: Bearer <token> when
auth is configured.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /status |
✗ public | Server uptime, DB connectivity check |
| GET | /exchanges |
required | Available ticker symbols per exchange |
| GET | /pairs |
required | Configured pairs with time bounds & tracked count |
| GET | /trades |
required | Trade data (filtered by venue, symbol, time range) |
| GET | /trades.arrow |
required | Trade data as Arrow IPC stream |
Returns the server health status. No authentication required.
| Field | Type | Description |
|---|---|---|
status |
string | Always "ok" while running |
uptime_secs |
int | Seconds since server start |
db_ok |
bool | true if DuckDB is reachable |
curl http://127.0.0.1:8080/status{
"status": "ok",
"uptime_secs": 7,
"db_ok": true
}Returns every ticker symbol discovered on each exchange, grouped by canonical exchange name. Useful for browsing available tickers before configuring the whitelist.
curl -H "Authorization: Bearer <token>" \
http://127.0.0.1:8080/exchanges{
"exchanges": {
"Binance Linear": ["BTCUSDT", "ETHUSDT", ...],
"Binance Spot": ["BTCUSDT", "ETHUSDT", ...],
"Bybit Linear": ["BTCUSDT", ...],
...
}
}
Returns all configured pairs with their stored time ranges. Pairs that have
been configured but have not yet received any trades appear with earliest
and latest as null.
| Field | Type | Description |
|---|---|---|
pairs |
array | Array of tracked pair objects |
tracked_count |
int | Total number of configured pairs |
Each pair object:
| Field | Type | Description |
|---|---|---|
ticker |
string | Ticker ID (Exchange:pair) |
earliest |
int | Unix ms of oldest stored trade, or null |
latest |
int | Unix ms of newest stored trade, or null |
curl -H "Authorization: Bearer <token>" \
http://127.0.0.1:8080/pairs{
"pairs": [
{
"ticker": "BinanceSpot:btcusdt",
"earliest": 1783873393043,
"latest": 1784372262447
},
{
"ticker": "HyperliquidLinear:btcusdc",
"earliest": 1784372175005,
"latest": 1784372262010
}
],
"tracked_count": 18
}Returns trade records matching the given filter.
| Param | Type | Description |
|---|---|---|
venue |
string | Required. Exchange name (e.g. binance) |
market |
string | Required. spot, linear, or inverse |
symbol |
string | Required. Ticker symbol (e.g. btcusdt) |
from |
int | Unix ms lower bound (inclusive) |
to |
int | Unix ms upper bound (inclusive) |
limit |
int | Max records (default 1000, max 10 000) |
Sort order: When
fromis specified, rows are returned oldest-first. Whenfromis omitted, rows are returned newest-first.
| Field | Type | Description |
|---|---|---|
trades |
array | Array of matching trade objects |
Each trade object:
| Field | Type | Description |
|---|---|---|
ts |
int | Unix millisecond timestamp |
price |
float | Price |
qty |
float | Normalized quantity (base units for spot/linear, quote notional for inverse perps) |
is_sell |
bool | true if a sell, false if a buy |
curl -H "Authorization: Bearer <token>" \
'http://127.0.0.1:8080/trades?venue=bybit&market=linear&symbol=btcusdt&limit=2'{
"trades": [
{
"ts": 1784108317317,
"price": 64774.8,
"qty": 0.111,
"is_sell": false
},
{
"ts": 1784108317964,
"price": 64774.7,
"qty": 0.003,
"is_sell": true
}
]
}Same filtering as /trades but returns an Arrow IPC streaming
format payload (Content-Type: application/vnd.apache.arrow.stream
with Content-Disposition: attachment; filename="trades.arrow")
with 4 columns:
ts (int64),price (float64),qty (float64),is_sell (bool)
This is ideal for high-volume data transfer to clients that support Arrow natively.
| Param | Type | Description |
|---|---|---|
venue |
string | Required. Exchange name (e.g. binance) |
market |
string | Required. spot, linear, or inverse |
symbol |
string | Required. Ticker symbol (e.g. btcusdt) |
from |
int | Unix ms lower bound (inclusive) |
to |
int | Unix ms upper bound (inclusive) |
limit |
int | Max records (default 50 000, max 400 000) |
Sort order: Same as
/trades.