Example open-source implementation of a delta-neutral funding-rate arbitrage bot over the VOOI Perps API (Hyperliquid + Lighter).
Disclaimer>
These examples are provided for educational purposes only. They are not financial advice and do not guarantee profit.>
The current bot examples were generated with AI-assisted development and tested before publication. They demonstrate how VOOI Ultra users can quickly prototype trading bots and agentic trading workflows using VOOI Perps API access available through VOOI Ultra.>
Testing does not make the bots risk-free. Trading bots can place real orders and interact with real funds. Review the code, configuration, strategy, and risk controls before running any bot with live capital.
vooi-funding-arb-bot
A production-grade Python bot for delta-neutral funding-rate arbitrage on perpetual futures, running through the VOOI Perps API. Trades Hyperliquid and Lighter today; Aster supported in code, off by default.
Real money software. This bot places real orders against real funds. It can lose money. Readdocs/STRATEGY.mdand the disclaimer inLICENSEbefore running it with live capital.
What it does in one paragraph
The bot scans funding-rate spreads across two exchanges every hour. When it finds a pair where the spread is large enough to pay back trading friction within a reasonable hold time, it opens a long position on the exchange paying out funding and a short position on the exchange charging in โ same asset, same size, opposite sides. The two legs cancel each other's price exposure; the bot collects the funding-rate difference for as long as the spread holds. It closes positions when the spread decays, reverses, or the position hits stop-loss thresholds.
That is the entire strategy. Everything else in this codebase is risk management around it.
When this bot is useful
- You have a VOOI Perps account with balance on at least two venues.
- You want to earn yield from cross-venue funding-rate spreads without taking directional risk.
- You're willing to lose 10โ20% of position notional in worst-case scenarios (venue outages, half-leg fills, sudden funding flips).
- You'll monitor the bot โ daily, not 24/7. It's autonomous but it's not foolproof.
When this bot is not useful
- You don't have a VOOI account or can't connect at least two venues.
- You want directional exposure to crypto. (This bot is delta-neutral by design.)
- You expect double-digit monthly returns. Realistic ballpark: low single-digit percentages monthly, highly variable.
- You won't read logs. The bot emits structured NDJSON for a reason โ it's the only way to understand what it did and why.
Running the bot
Two supported paths are documented inline below: local (macOS/Linux dev machine, foreground or nohup) and dedicated Linux VPS (systemd unit, autostart, log rotation). For Docker and Fly.io, see docs/DEPLOY.md.
Before either path, prepare:
- A VOOI Perps account with balance on at least two venues.
- Your VOOI bearer token โ get it at ultra.vooi.io/api-tokens.
Run locally (macOS / Linux)
Good for first-time setup, dry-runs, monitoring the bot from your dev machine.
# 1. Install uv (Python package manager)
macOS:
brew install uv
Linux (or macOS without Homebrew):
curl -LsSf https://astral.sh/uv/install.sh | sh
2. Clone and install
git clone https://github.com/your-org/vooi-funding-arb-bot
cd vooi-funding-arb-bot
uv sync --extra dev
3. Configure
cp .env.example .env
$EDITOR .env # fill VOOIBEARERTOKEN
4. Validate the API contract (optional, recommended on first run)
uv run python -m probe.probe readonly
5. Dry-run โ no orders, just decisions written to the log
BOTDRYRUN=true uv run python -m fundbot
6. Live โ only after you've watched a few dry-run cycles
BOTDRYRUN=false uv run python -m fundbot
To background the bot in the same shell session without systemd:
nohup uv run python -m fundbot >> bot.log 2>&1 &
echo $! > /tmp/vooi-funding-arb-bot.pid
disown
Stop gracefully (current cycle finishes, state is persisted):
kill -TERM "$(cat /tmp/vooi-funding-arb-bot.pid)"
Tail logs while running:
tail -f bot.log # human-readable stdout
tail -f state.ndjson # one JSON event per line (machine-readable)
Run on a dedicated VPS (Linux, systemd)
Recommended for live trading: the bot is a long-running process and benefits from a supervised, restart-on-failure environment. Instructions are written for Ubuntu 22.04 / 24.04 or Debian 12 โ adapt the package manager for other distros.
1. Provision the box. A 1 vCPU / 1 GB RAM VPS is enough. Pick a region close to the VOOI API for latency (e.g. Tokyo, Frankfurt). Make sure outbound HTTPS to perps-api.vooi.io is allowed.
2. Create a dedicated system user. The bot does not need root.
sudo adduser --system --group --shell /bin/bash --home /opt/vooi-arb vooi-arb
sudo install -d -o vooi-arb -g vooi-arb /opt/vooi-arb /var/lib/vooi-arb /var/log/vooi-arb
/var/lib/vooi-arb will hold state files; /var/log/vooi-arb will hold rotated logs.
3. Install Python and uv.
sudo apt update
sudo apt install -y python3.11 python3.11-venv git curl ca-certificates
sudo -u vooi-arb -H bash -c 'curl -LsSf https://astral.sh/uv/install.sh | sh'
uv lands at /opt/vooi-arb/.local/bin/uv. Add it to the bot user's PATH (done via the systemd unit below).
4. Clone the repo and install dependencies.
sudo -u vooi-arb -H git clone https://github.com/your-org/vooi-funding-arb-bot /opt/vooi-arb/app
cd /opt/vooi-arb/app
sudo -u vooi-arb -H /opt/vooi-arb/.local/bin/uv sync
5. Configure .env. Permissions matter โ the file holds your bearer token.
sudo -u vooi-arb -H cp /opt/vooi-arb/app/.env.example /opt/vooi-arb/app/.env
sudo -u vooi-arb -H $EDITOR /opt/vooi-arb/app/.env
sudo chmod 600 /opt/vooi-arb/app/.env
Set in .env (the values shown move state out of the repo and into the var dir):
BOTDRYRUN=true
BOTSTATEFILE=/var/lib/vooi-arb/state.ndjson
BOTSNAPSHOTFILE=/var/lib/vooi-arb/state-snapshot.json
BOTCOOLDOWNFILE=/var/lib/vooi-arb/state-cooldown.json
BOTPIDFILE=/var/lib/vooi-arb/bot.pid
6. (Optional) Run probe + dry-run once manually before installing the service.
sudo -u vooi-arb -H bash -c 'cd /opt/vooi-arb/app && /opt/vooi-arb/.local/bin/uv run python -m probe.probe readonly'
sudo -u vooi-arb -H bash -c 'cd /opt/vooi-arb/app && BOTDRYRUN=true /opt/vooi-arb/.local/bin/uv run python -m fundbot' &
Watch /var/lib/vooi-arb/state.ndjson for a few cycles, then Ctrl-C.
7. Install the systemd unit. Write /etc/systemd/system/vooi-arb.service:
[Unit]
Description=vooi-funding-arb-bot
After=network-online.target
Wants=network-online.target
[Service] Type=simple User=vooi-arb Group=vooi-arb WorkingDirectory=/opt/vooi-arb/app EnvironmentFile=/opt/vooi-arb/app/.env Environment=PATH=/opt/vooi-arb/.local/bin:/usr/local/bin:/usr/bin:/bin ExecStart=/opt/vooi-arb/.local/bin/uv run python -m fundbot Restart=on-failure RestartSec=15 KillSignal=SIGTERM TimeoutStopSec=120 StandardOutput=append:/var/log/vooi-arb/bot.log StandardError=append:/var/log/vooi-arb/bot.log
Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/vooi-arb /var/log/vooi-arb /opt/vooi-arb/app
[Install] WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now vooi-arb.service
sudo systemctl status vooi-arb.service
8. Operate the service.
# Tail human-readable bot output:
sudo tail -f /var/log/vooi-arb/bot.log
Tail structured events (one JSON per line):
sudo tail -f /var/lib/vooi-arb/state.ndjson
Restart (graceful SIGTERM; current cycle finishes first):
sudo systemctl restart vooi-arb.service
Stop:
sudo systemctl stop vooi-arb.service
9. Rotate logs. Add /etc/logrotate.d/vooi-arb:
/var/log/vooi-arb/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
su vooi-arb vooi-arb
}
10. Flip to live trading.
sudo -u vooi-arb -H sed -i 's/^BOTDRYRUN=.*/BOTDRYRUN=false/' /opt/vooi-arb/app/.env
sudo systemctl restart vooi-arb.service
Upgrades.
sudo systemctl stop vooi-arb.service
sudo -u vooi-arb -H bash -c 'cd /opt/vooi-arb/app && git pull && /opt/vooi-arb/.local/bin/uv sync'
Diff .env.example for new keys, update .env if needed.
sudo systemctl start vooi-arb.service
sudo journalctl -u vooi-arb.service -n 50 --no-pager
Health check. The bot's "is-alive" signal is the mtime of state.ndjson โ it ticks every monitor cycle (5 min by default). A 10-minute gap means the process is wedged. A minimal external check:
[[ $(($(date +%s) - $(stat -c %Y /var/lib/vooi-arb/state.ndjson))) -lt 600 ]]
For container-based deployment (Docker / Fly.io) and a deeper troubleshooting checklist, see docs/DEPLOY.md.
How it works (the algorithm at a glance)
Each cycle, the bot:
- Reads state from
state-snapshot.json(positions) andstate-cooldown.json(cooldown ledger). - Fetches live data from VOOI in parallel:
getfundingstrategies(ranked opportunities),getpositions(with funding accrual per leg),getaccounts(margin balances). - Reconciles state against reality. Detects positions closed externally, half-leg states, and orphan positions on the exchange that aren't in state.
- Updates per-arb history: appends the current netAPR reading to a rolling 24-entry list, updates funding accrued, updates the sticky
fundingbreakevenachievedflag. - Decides closes by walking a priority list โ
hardstoplossโmaxholdโminhold gateโsmartnegNโsmartdeclNโlowaprN. First match wins. - Decides opens by filtering the opportunity list โ APR band, volume floors, blacklist, cooldown, slippage estimate, basis check, per-venue margin cap. Survivors get bracket SL/TP placed alongside the entry order.
- Executes with
batchcreateorders(atomic two-leg open) or per-legcreate_orderfor closes. Broker / integrator attribution is set server-side by the VOOI API; the bot does not need to pass any builder/integrator IDs. - Writes state back atomically and emits an
NDJSONevent line summarizing the cycle.
docs/STRATEGY.md.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ VOOI Perps REST API โ โโ single integration point
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
โ fundbot/ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ mvp.py โ engine โ
โ โข cycle loop โ
โ โข opportunity ranking โ
โ โข close decision tree โ
โ โข limit-then-market entry โ
โ โข reconcile + orphan close โ
โ โข bracket SL/TP placement โ
โ โข NDJSON event log โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
โ state files โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ state.ndjson (events) โ
โ state-snapshot.json (resume) โ
โ state-cooldown.json (block) โ
โ instance.uuid (coid ns) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The bot is a single async Python process. No external services, no database, no message queue. State lives in JSON / NDJSON files on a writable volume. SIGTERM = graceful shutdown (current cycle finishes, then state writes).
A long-lived SSE connection to /exchange/updates (default on) lets the tight-loop pollers โ the 1-second survivor watcher and the 5-second ALO fill watchers โ wake on push events instead of strictly polling. REST stays the source of truth: every decision is still REST-confirmed, the stream just lets the bot react faster. See docs/SSE.md for details and disable instructions.
Companion tools
probe/โ a phased API validation tool you run before the bot. Confirms your token works, your account is funded, and the venues respond as expected. Seeprobe/README.md.scripts/โ operational helpers: close one arb, close all, recover snapshot from logs, show balances. Each is a small standalone Python script.skills/funding-arb-cycle/โ a Claude Code skill that re-implements the bot's strategy as an interactive co-pilot over the VOOI MCP. Same algorithm; you confirm each open/close manually. Useful as a learning tool or as a manual fallback when the bot is offline.
Configuration
All configuration is environment variables. The full list with defaults and inline comments is in .env.example. The keys you almost always need to touch:
| Variable | What it controls | Default | |---|---|---| | VOOIBEARERTOKEN | Auth to the VOOI API โ get it at ultra.vooi.io/api-tokens | (required) | | BOTTARGETEXCHANGES | Which venues to trade | hyperliquid,lighter | | BOTLEGCOLLAT_USD | Margin per leg | 10 | | BOTLEVERAGETARGET / _CAP | Target / hard-cap leverage | 10 / 5 | | BOTMINNET_APR | Don't open below this APR | 0.70 (= 70%) | | BOTMINHOLD_HOURS | Don't soft-close before this age | 12 | | BOTMAXHOLD_HOURS | Force-close at this age | 96 | | BOTSTOPLOSS_PCT | Hard-stop threshold | 0.05 | | BOTLOWAPRTHRESHOLD / WINDOW | Soft-close on APR decay | 0.10 / 6 | | BOTASSETBLACKLIST | Skip these symbols | (empty) | | BOTDRYRUN | If true, no orders are placed | true | | BOTSSEENABLED | SSE event stream โ latency optimisation over REST polling. REST stays canonical. See docs/SSE.md. | true |
The defaults are tuned to a small-capital environment ($100โ500 deployed). At larger size, friction-per-arb drops and you can profitably widen the opportunity pool โ see docs/STRATEGY.md ยง Why these defaults.
Safety model
The bot has several layers between you and a runaway loss:
BOTDRYRUN=trueby default. No order goes out unless you explicitly set it tofalse.hardstoplossโ per-arb dollar threshold checked every monitor cycle (5 min by default). Hits even beforemin_hold.maxholdโ every arb is force-closed afterBOTMAXHOLDHOURSregardless of P&L.BOTPAIRCOOLDOWN_*โ assets that hurt you twice in 48h are paused.- Per-venue margin cap โ bot refuses to open if it would exceed
BOTMAXMARGINPEREXCHANGE_USD. - Bracket SL/TP โ every open carries on-venue stop-loss and take-profit orders. They survive the bot going down.
- Single-instance lock โ PID file at
BOTPIDFILEprevents accidentally running two copies on the same token. - Atomic state writes โ
.tmp+ rename, so a crash mid-write doesn't corrupt your snapshot. - Graceful shutdown โ SIGTERM finishes the current cycle, persists state, and exits cleanly.
- SSE is strictly additive. REST is canonical. If the event stream crashes, silences, or is rejected, callers fall through to their original poll-based behaviour with no state change. See
docs/SSE.mdยง Safety properties.
state.ndjson daily.
What's in this repo
.
โโโ README.md # this file
โโโ LICENSE # MIT + disclaimer
โโโ CONTRIBUTING.md # how to send a PR
โโโ AGENTS.md # contract for AI agents working in this repo
โโโ .env.example # config template โ copy to .env
โโโ pyproject.toml # Python project manifest
โโโ uv.lock # locked dependencies (re-lock with uv lock)
โโโ Dockerfile # production container
โโโ .dockerignore
โโโ .gitignore
โ
โโโ fundbot/ # the bot
โ โโโ main.py # python -m fundbot entry point
โ โโโ mvp.py # the engine โ one file, ~3700 lines
โ
โโโ probe/ # phase-0 API validator (run before first trade)
โ โโโ README.md
โ
โโโ scripts/ # operational helpers
โ โโโ closeone.py # close a specific arbid
โ โโโ close_all.py # emergency unwind all positions
โ โโโ close_orphan.py # close one orphan leg by asset+exchange
โ โโโ recover_snapshot.py # rebuild state-snapshot.json from state.ndjson
โ โโโ show_balances.py # one-shot balance dump
โ โโโ trade_analysis.py # diagnostic stats from logs
โ โโโ realizedpnlfrom_log.py
โ โโโ cycle_report.py
โ โโโ monitor.py
โ โโโ plotportfoliofrom_log.py
โ
โโโ tests/ # pytest suite
โ
โโโ skills/ # Claude Code skills
โ โโโ funding-arb-cycle/ # MCP co-pilot: same strategy, human-in-the-loop
โ โโโ SKILL.md
โ โโโ README.md
โ
โโโ docs/
โโโ STRATEGY.md # full algorithm and tuning rationale
โโโ DEPLOY.md # local / Docker / Fly.io deployment
For AI agents
If you are an AI assistant (Claude, Cursor, Aider, etc.) about to make changes here, read AGENTS.md. It encodes the safety rules that humans expect you to follow.
The companion Claude Code skill at skills/funding-arb-cycle/ lets you (or an end user) drive the same strategy through the VOOI MCP server, with explicit per-action confirmation. It is a separate product โ useful when:
- You don't want a long-running process.
- You want to run on a
/loop 60mcadence with a human approving each cycle. - You're learning the strategy and want to step through decisions interactively.
Roadmap (suggestions; PRs welcome)
- Aster integration polish โ funding-fee field is null on Aster positions; need a fallback that estimates funding from
getfundingspreadchartร heldh. - Per-asset config โ currently filters are global. Some symbols deserve tighter / looser thresholds.
- Historical spread for safe-floor โ replace the rolling
aprhistorywith on-demandgetfundingspreadchartfor a smarter floor calculation. - Web UI / dashboard โ none today. The bot is fine without one, but daily monitoring is friction.
- Telegram / Discord notifications โ emit events on open / close / hard-stop. Not in core; should be a plug-in.
CONTRIBUTING.md before sending PRs.
Links
- VOOI Perps API docs: https://perps-api.vooi.io/docs
- VOOI Perps OpenAPI JSON: https://perps-api.vooi.io/swagger/json
- VOOI Perps MCP server: https://perps-api.vooi.io/mcp
- Strategy reference:
docs/STRATEGY.md - Deployment:
docs/DEPLOY.md - Issues / discussion: see project Homepage in
pyproject.toml
License
MIT. See LICENSE.
This is real-money software. Read the disclaimer.