Library / SDK
beibei030/classic-grid avatar
beibei030/classic-grid

classic-grid: an eight-venue perp grid bot built around one rule, a restart must not cancel your book

Classic grid bot: Extended / RISEx / Decibel / N1 / Phoenix

333 stars108 forksTypeScriptMIT

At a glance

What is it?
beibei030/classic-grid is a TypeScript template that seeds an arithmetic grid across Extended, RISEx, Decibel, N1, Phoenix, Phoenix2, Nado and PopDEX, refills the adjacent level after each fill, and ships a dry-run mode plus a dashboard at 127.0.0.1:8088.
Who is it for?
classic-grid is worth reading if you want a working multi-venue grid reference rather than a strategy to adopt, because the per-venue adapter split, the SOFT_RESUME anchor and the documented failure modes are the reusable parts. Do not point it at real money on a first run: run `DRY_RUN=1 npm start -- --once` first and read what it prints, and remember that going live needs both `DRY_RUN=0` and `LIVE_CONFIRM=YES` set at the same time.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 44 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The grid refills the adjacent level instead of rebuilding the whole book

The strategy is an arithmetic grid, and the mechanics are given in one line: buy below the current price and sell above it, then after a fill place the adjacent opposite order. The detail that matters is in the challenges table, where a post-fill grid gap is solved by buying then placing the upper neighbour sell, or selling then placing the lower neighbour buy, one order per level.

That is a per-level refill rather than a periodic rebuild, and it is why the strategy logic lives in a single file, `src/grid.ts`, alongside a `skipBand` that skips a band of width near the current price. The main loop in `src/loop.ts` is snapshot, then plan, then apply, so the bot reads exchange state before it decides anything.

Startup is where the risk checks live. The bot verifies that grid spacing exceeds both-side fees before it starts placing anything, and it runs a margin pre-check. There is a `GRID_MARGIN_FRAC` setting, 0.7 by default, which means margin is sized at 70 percent of the budget, and a `GRID_LEVERAGE` global that defaults to 30. Getting the spacing wrong relative to fees is the classic way an arithmetic grid quietly loses money, so that check is the first thing to inspect when you read the source.

SOFT_RESUME exists so a restart does not cancel your whole order book

Most grid implementations treat a restart as a clean slate, which on a live venue means either duplicating every level or cancelling everything and starting again. This template takes the opposite approach. `SOFT_RESUME` is on by default, and it writes a local anchor so that a restart only fills gaps rather than clearing the book.

The challenges table names the failure it addresses as restarting and wiping the pending orders, with the fix given as `SOFT_RESUME` plus a local anchor that only tops up what is missing. The difference in behaviour is large: a hard reset on a venue with eighty levels costs you the spread you were holding and re-crosses the book at whatever prices are current.

The reason this needs to be a first-class setting rather than a startup flag is the emergency control surface. `src/botControl.ts` handles emergency pause and resume, exposed over the dashboard as `POST /api/pause` and `POST /api/resume`, and the adapter interface makes `cancelAll` and `closePosition` optional capabilities per venue. A bot that can flatten is useful; a bot that flattens on every restart is not, so the two behaviours are kept separate on purpose.

DRY_RUN=1 and LIVE_CONFIRM=YES form a two-key interlock

The live trading gate is two environment variables that must both be satisfied, and defaults point away from trading. `DRY_RUN` defaults to 1, meaning simulate with a read-only dashboard and no orders placed, and `LIVE_CONFIRM` starts empty.

bash
npm install
cp .env.example .env
DRY_RUN=1 npm start -- --once

The `--once` flag is what makes the first run safe to actually read. It prints each venue's anchor, grid count, spacing, size and risk pre-check, and then stops without placing an order. Only when that output looks right do you go live, which requires both keys flipped:

bash
DRY_RUN=0 LIVE_CONFIRM=YES npm start

Prerequisites are Node.js 20 or newer and at least one exchange account with API keys, though the dry run works with no keys at all so you can watch the flow. The dashboard listens on `http://127.0.0.1:8088/` by default, and `/api/snapshot` is the machine-readable version of the same data. A machine-readable snapshot plus explicit pause and resume endpoints means you can bolt an external kill switch onto this without touching the strategy code.

P&L definitions disagree across venues, so the accounting path is per-venue

The most useful thing in `docs/CHALLENGES.md` is the row about inconsistent profit and loss definitions. The resolution given is that Extended reads its closed-position history, while RISEx and Decibel use fill-level realized figures instead. There is no single number that means the same thing across all eight venues, so the adapter carries the difference.

A related row handles the case where position notional differs a lot between venues, because the net position path is different for each. The approach is to reconcile the full-grid notional first and only then the net grid count, rather than trying to match a grid level count that does not mean the same thing on different books.

Two more rows are about encoding rather than strategy. Decibel's tick and lot size have to be aligned before values are encoded, which is the classic precision bug in a perp API, and PopDEX is an on-chain CLOB handled with viem signing and a gasless relay broadcast, so it is the one adapter with a materially different transaction path from the rest. The last row is about bad JSON responses: they are classified as transient soft errors, they do not trigger Telegram spam, and they are retried on the next round.

Eight venues ship different chains, key types and default risk numbers

The default parameters table is the fastest way to see how much variation the adapter layer is absorbing. Seven venues run 80 grid levels at 30x leverage. Extended uses a half band of plus or minus 4.6 percent on Starknet with an 800U budget at 70 percent. Decibel and N1 use plus or minus 5 percent, on Aptos and Solana respectively, and N1 is marked PostOnly. Phoenix, Phoenix2, Nado and PopDEX all use plus or minus 4.5 percent, with Phoenix2 running an independent keypair alongside Phoenix, Nado on the Ink chain, and PopDEX on Morph Tachyon.

RISEx is the outlier at 46 levels, 25x and plus or minus 3 percent, annotated as having a larger single order and needing risk reduced.

Every one of those numbers is overridable. Per-venue `*_LEVERAGE`, `*_HALF_BAND` and `*_EQUITY_USD` variables exist for Extended, RISEx, Decibel, N1, Phoenix and Nado, and the global set includes `MARKETS=BTC`, `TICK_MS=15000` and `DASHBOARD_PORT=8088`. Credentials differ just as much: Extended wants an API key, a Stark private and public key and a vault ID, Decibel wants an account private key plus a subaccount and a gas station key, N1 reads a keypair from `secrets/id.json` by default, and Phoenix reads `secrets/phoenix.key`. Those paths are already in `.gitignore`.

Rate limits and write spacing are per-venue knobs, not one global setting

Rate limiting and order caps get their own row in the challenges table, with four controls named: `maxOpenOrders`, write frequency, spacing, and error deduplication. The spacing values in the environment template show how far apart the venues are. `RISE_ORDER_GAP_MS` is 10500, so more than ten seconds between writes on RISEx, while `PHOENIX_ORDER_GAP_MS` is 800 and `EXTENDED_ORDER_GAP_MS` is left empty for a default. There is also a `GRID_SKIP_LEVERAGE` global and a RISEx-specific `RISE_SKIP_LEVERAGE`, both defaulting to 0.

A related row covers the official statistics pull exhausting memory. The fix given is throttled fetching plus a larger Node heap, which is why `src/officialStats.ts` exists as its own module for official volume, fees and closed-position P&L. With eight venues being polled on a 15-second tick, that is the difference between a dashboard and an out-of-memory crash.

Telegram sits on the same side of the system and is entirely optional. Three variables in `.env` control it:

env
TELEGRAM_ENABLED=true
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_IDS=

The token comes from BotFather with `/newbot`, and the chat IDs come from sending the bot any message and reading `chat.id` back out of the `getUpdates` response, with several IDs separated by commas. When the flag is off or either value is empty, the program runs normally and simply sends nothing, which is the right default for a bot you are still evaluating.

Five npm scripts, no build step, and a template that ships without secrets

There is no compilation step. All five scripts run TypeScript directly through the `tsx` loader:

bash
npm start

which maps to `node --import tsx src/cli/run.ts`. Alongside it sit `status`, `flat`, `dashboard` and `test`, the last pointing at `test/grid.test.ts`, so the grid core has a test entry point even though the surface is small. The manifest is marked private at version 0.2.0, which is another way of saying this is a template you clone rather than a package you install.

The source layout is organised so a reader knows where to look. Strategy is `src/grid.ts`, the main loop is `src/loop.ts`, level and anchor configuration plus environment parsing is `src/config.ts`, the dashboard service is `src/dashboard.ts`, daily P&L and deposit accounting is `src/ledger.ts`, and per-venue adapters sit in `src/venues/` as `extended.ts`, `risex.ts`, `decibel.ts`, `decibelLive.ts`, `n1.ts`, `phoenix.ts`, `nado.ts` and `popdex.ts` behind a shared interface. The dashboard frontend is a plain static `public/index.html`, and a standalone demo page sits at `docs/demo-dashboard.html` for looking at the interface without running anything.

The repository is explicit that the template contains no private keys, API keys, Telegram tokens, server addresses or ledger files, and that `.env`, `secrets/` and `data/` must never be committed. It also carries registration and referral links for the eight venues, which the README labels as optional and not investment advice. The licence is MIT.

Editorial conclusion

classic-grid is worth reading if you want a working multi-venue grid reference rather than a strategy to adopt, because the per-venue adapter split, the SOFT_RESUME anchor and the documented failure modes are the reusable parts. Do not point it at real money on a first run: run `DRY_RUN=1 npm start -- --once` first and read what it prints, and remember that going live needs both `DRY_RUN=0` and `LIVE_CONFIRM=YES` set at the same time. Before you enable any venue, check three things: that its key type matches what the adapter expects, since Extended wants a Stark keypair, Decibel an account private key, and Phoenix and N1 a Solana keypair; that its rate limit spacing is right, since RISEx needs over ten seconds between writes while Phoenix is set to 800 milliseconds; and that `.env`, `secrets/` and `data/` never reach a commit. The manifest is private at version 0.2.0, the licence is MIT, and the repository is not archived with a last push on 2026-08-19.

Frequently asked questions

What does classic-grid do and which venues does it support?

It is an arithmetic grid trading template that seeds levels, buys below the current price and sells above it, then places the adjacent opposite order after each fill. Adapters exist for Extended, RISEx, Decibel, N1, Phoenix, Phoenix2, Nado and PopDEX, behind a shared VenueExecutor interface with snapshot, apply and optional cancelAll and closePosition.

How do I run classic-grid without placing real orders?

Leave DRY_RUN=1, which is the default, and add the once flag: DRY_RUN=1 npm start -- --once. It prints each venue's anchor, grid count, spacing, size and risk pre-check and places no orders. It needs no API keys at all. The dashboard is then on http://127.0.0.1:8088/ with /api/snapshot as the machine-readable version.

What does SOFT_RESUME do in classic-grid?

It is a restart recovery anchor, enabled by default, so a restart only tops up missing orders instead of cancelling the whole book. The documented failure it addresses is a restart wiping pending orders. Emergency control is separate, exposed as POST /api/pause and POST /api/resume through the dashboard.

How do I enable Telegram notifications in classic-grid?

Set TELEGRAM_ENABLED=true plus TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_IDS in your local .env. The token comes from BotFather using /newbot, and chat IDs come from sending the bot a message and reading chat.id out of the getUpdates response, with several IDs comma separated. If the flag is off or a value is empty the program runs normally and sends nothing.

Why does classic-grid need both DRY_RUN=0 and LIVE_CONFIRM=YES?

Because live trading requires both conditions to be satisfied at once. DRY_RUN defaults to 1, which means simulate with a read-only dashboard and no orders, and LIVE_CONFIRM starts empty, so a single variable change cannot switch the bot into placing real orders.

Official sources

  1. beibei030/classic-grid on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/beibei030-classic-grid.svg)](https://hysenlabs.com/projects/beibei030-classic-grid)