The boulder always rolls back down. We profit on the way.
A self-hosted, fee-aware grid trading bot for Binance Spot, running on your own PC. Static-range grid, maker-only orders (LIMIT_MAKER), with a deterministic two-layer regime gate that pauses trading in hostile conditions and hard-halts on real danger.
Honest framing: this is a validation and learning project, not an income system. Grid trading harvests volatility and loses money in strong trends. The gate logic is deterministic risk management, not prediction. There is deliberately no martingale or double-down logic, and there never will be.
Authoritative design document: BUILD_SPEC.md (includes the v2 measurement/upgrade sections, BUILD_SPEC.md §12–16).
All ten base build steps (BUILD_SPEC.md Section 10) plus the full autopilot and
v2 measurement layer (Sections 11–16, steps 11–22) are implemented and tested —
270 passing tests including the kill/restart chaos suite, the rotation state
machine, and a 6-agent correctness review with every critical/high finding
fixed (see CODEBASE_REVIEW.md). Stage B autopilot
(automatic rotation) is active on testnet. Live trading remains locked
behind the promotion gates in Section 9 — useTestnet stays true until a
human deliberately flips it.
Known limitation: commissions paid in an asset that's neither the pair's quote
nor base currency (e.g. BNB, if the fee discount is active) are not converted
to quote value for PnL — the bot warns the first time it sees this, and PnL is
slightly overstated until it's addressed. Separately, the bot cross-checks the
BNB discount configuration (feePctPerSide) against the actual fee asset on
the first live fill and warns loudly on any mismatch.
- Node.js ≥ 22
- A Binance account (start on the Spot Testnet — the bot defaults to testnet and stays there until you explicitly flip it)
These steps are non-negotiable. A key created wrong is the single biggest risk in running this bot.
- Testnet first. Create a key at https://testnet.binance.vision and complete validation Phase 1 (see below) before you even think about a live key.
- When (and only when) you create a live key:
- Enable Spot trading only. Do not enable margin, futures, or transfers.
- Withdrawals: DISABLED. The bot never needs them; a leaked key must not be able to move funds off the exchange.
- IP whitelist: restrict the key to the public IP of the machine running Sisyphus. An un-whitelisted key is disabled by Binance after 90 days for good reason.
- Copy
.env.exampleto.envand fill in the key and secret..envis git-ignored from the first commit — keep it that way. - If a key ever ends up in a commit, a log, or a paste: revoke and rotate it immediately. History rewriting is not sufficient.
Every grid cycle pays two maker fees (buy + sell). At Binance's standard 0.10% per side that is 0.20% round trip — a meaningful bite out of a 1% grid step. Enable the BNB fee discount (hold a small BNB balance and turn on "Use BNB for fees"): 0.075% per side instead of 0.10%. The startup spacing check enforces that your grid step is at least minSpacingFeeMultiple × the round-trip fee, and will refuse to start otherwise. The bot nags at startup while feePctPerSide is above 0.00075, and the first real fill cross-checks the actual fee asset against the config — a mismatch in either direction is warned about loudly.
Zero-fee pairs (13.1): Binance periodically runs zero-maker-fee promos
(historically FDUSD-quoted). Point the scanner at them with "quoteAsset": "FDUSD" and declare the promo fee per symbol: "feeOverridesBySymbol": { "BTCFDUSD": 0 }. A zero fee collapses the fee-derived spacing rule, so an
absolute floor takes over: steps can never go below minStepAbsPct (default
0.15%) — below that, spread and tick noise eat the cycle no matter what fees
are. The scanner scores each symbol at its own fee-adjusted sMin, so a 0-fee
pair with modest oscillation can legitimately outrank a fee-paying mover.
The interactive wizard asks for everything (mode, symbol, grid range with a
suggestion from the live price, sizes, fees, Telegram, API keys with hidden
input), validates with the same rules the bot enforces at startup, and writes
gridbot.config.json + .env (created with 0600 permissions):
npm install
npm run setupPrefer doing it by hand? The equivalent is:
cp .env.example .env # then fill in your credentials
cp gridbot.config.example.json gridbot.config.jsonEdit gridbot.config.json and set lowerBound / upperBound around the current market price — they are the only required fields and have no defaults. Every other key is optional and falls back to the spec defaults (BUILD_SPEC.md Section 5). Unknown keys are rejected on purpose, so typos fail loudly instead of silently using a default.
Validation refuses to start (listing all problems at once) if, among other things:
- the grid step at the top of the range is less than
minSpacingFeeMultiple× the round-trip fee — a grid tighter than this pays more in fees than it earns per cycle; numGrids × quotePerOrderexceedstotalQuoteCapital;- bounds are missing, inverted, or any value is out of range.
At startup the bot logs a 12-character config fingerprint so every run is attributable to an exact configuration. useTestnet defaults to true; going live is an explicit, deliberate edit.
npm start # the Sisyphus app: a menu for Setup / Start / Flatten / Events —
# nothing to remember, arrow keys + Enter
npm run bot # run the trading engine directly (dashboard in a TTY,
# headless logs otherwise — e.g. under nohup)
npm run flatten # standalone kill switch: cancel ALL open orders and exit.
# Works even if the main process is dead.
npm run report # performance report: cycles, PnL, banked profit
npm run scan # autopilot Stage A: score candidate pairs, health-check the
# incumbent, print the R1-R6 rotation checklist
npm run rotate # switch pairs via the crash-safe rotation state machine
# (-- --to SYMBOL to override the scan's pick,
# -- --rerange to keep the symbol and re-center the range,
# -- --dispose to authorize maker-only inventory disposal)
npm run lab # shadow lab: N parameter variants trading the same live
# stream in parallel, one DB per variant (see below)
npm run backtest # replay historical 1m klines through the same variants
# (-- --days 30); ranks parameters incl. trend periods
npm test # unit tests + chaos suiteSwitching pairs: never just edit symbol in the config — the exchange still
holds the old pair's orders and inventory, and startup will refuse. npm run rotate does flatten → reconcile → reconfigure in the right order and survives
crashes at any step (rerun it to resume). The setup wizard warns if you try the
config-only route.
Autopilot (Stage B): with "autopilotEnabled": true (requires
"pauseBuysOnRangeExit": true), the engine scans every scanIntervalHours and
rotates automatically when ALL of R1-R6 pass; otherwise it stays and logs why.
When rotation says stay but the range has merely gone stale (price out of range
≥33% of the day, trend filter green, inventory small), it re-ranges in place
instead (13.3) — same crash-safe machine, same symbol, new bounds. Re-ranges
have their own dwell (48h) and budget (4/30d); a near-halt unrealized loss
always blocks re-ranging, because a drifted range with heavy inventory is a
trend symptom, not a stale range.
When the incumbent has been unhealthy ≥ 72 h but a rule blocks rotation, it
sends one Telegram alert per 24 h naming the blocker. The dashboard and the
30 s status line show the next scan time and the last decision.
Dashboard keys: k twice within 3 s = kill switch (cancel all, halt). q = graceful shutdown (resting orders are left on the exchange — the screen says so).
Startup refuses to run unless everything checks out: credentials present, config
valid (fingerprint logged), quotePerOrder ≥ 1.5× the symbol's minNotional,
price inside the grid range with at least one grid step of margin, and local DB
reconciled against the exchange. A reconciliation mismatch that can't be repaired
mechanically prints a full diff and refuses to trade.
With no TTY (e.g. nohup), the dashboard is skipped and a status line is logged
every 30 s instead. State lives in gridbot.db (gridbot.shadow.db in shadow
mode); deleting it is safe only while no orders are resting.
Troubleshooting: everything the bot does is appended to gridbot.log
(git-ignored) — startup checks, reconcile reports, every order/gate/risk/ws
event, a status heartbeat every 30 s, halts with reasons, flatten runs, and
refusals to start. One timestamped line per event, so grep halt gridbot.log
or grep -c "\[cycle\]" answer most questions. It rotates at 5 MB (one .1
generation kept) and never contains credentials.
Shadow lab (BUILD_SPEC 12.2): cp lab.config.example.json lab.config.json,
edit the variants (config overrides per label — bounds, numGrids, quotePerOrder;
symbol is pinned to the base config), then npm run lab. Each variant runs the
full engine in shadow mode against the same live stream and writes to its own
gridbot.shadow.<label>.db. It is read-only with respect to the exchange and
safe to run alongside the real bot. npm run lab -- --report prints the daily
comparison (net, cycles, capture ratio per variant). Decision rule, fixed in
advance: a variant must beat the incumbent's net PnL over ≥ 14 days AND ≥ 30
completed cycles before its parameters are adopted — and adoption is a manual
config change. Caveat: shadow fills are simulated by price-cross and ignore
queue position, so fill rates are slightly optimistic — uniformly across
variants; valid for comparing them, weak for projecting income.
Try it without keys: set "shadowMode": true in gridbot.config.json and use
any dummy values for the Binance env vars — shadow mode runs the full engine
against live market data and only stubs the final order call, simulating fills
(with fees) when price crosses a level.
- Credentials via environment variables only — never logged, never stored in the database, never in error messages.
- No inbound network surface: no web server, no open ports, no remote control. Telegram is outbound-only notifications.
- Exactly four pinned runtime dependencies (
better-sqlite3,ink,react,ws), lockfile committed,npm auditrun locally. No third-party Binance wrapper — signing is done directly withnode:crypto. - Market data is trusted only from Binance's own endpoints and streams. No third-party feeds, no webhooks, no signal inputs.
| Phase | Environment | Duration | What must pass |
|---|---|---|---|
| 1 | Binance Spot Testnet | 1–2 weeks | Full order lifecycle, clean kill/restart reconciliation ×10, all halt paths fire and alert |
| 2 | Shadow mode on live data | 1 week | Realistic fill/gate statistics; gate goes red during a genuinely volatile period |
| 3 | Live micro ($10 orders, ≤$120 total) | 4+ weeks | Zero reconciliation mismatches; honest net-of-fee accounting (profit not required — correctness is) |
| 4 | Scaling discussion | — | Only after 1–3 pass. Separate decision. |
You run this at your own risk, with money you can afford to lose entirely. Nothing here is financial advice.