Build a Risk Gated Bybit Perpetuals Trading Bot
Create a testnet first Codex workflow with read only market access, deterministic trade approval, persistent memory, audit logs, cron scheduling, kill switch protection, and Telegram control
Download the complete Bybit trading bot guide
Build a Risk Gated Bybit Perpetuals Trading Bot
A testnet first implementation blueprint using Codex, Bybit MCP, persistent memory, deterministic risk checks, cron, and Telegram controls
What you will achieve
You will generate and configure a Bybit USDT perpetuals bot in which the model analyzes markets and proposes trades, while a separate code based gate approves, rejects, executes, and audits every exchange write.
Who this is for
Developers and technically confident traders who understand perpetual futures and want to prototype an autonomous trading workflow without giving the model unrestricted exchange access.
Difficulty: Advanced
Short tutorial
A testnet first implementation blueprint using Codex, Bybit MCP, persistent memory, deterministic risk checks, cron, and Telegram controls
Guide: Build a Risk Gated AI Trading Bot for Bybit Perpetuals
Time: 2 to 4 hours for initial testnet setup,
What you will build
This tutorial gives you a complete blueprint for a testnet first trading bot on Bybit USDT perpetuals. The model acts as the decision engine, Bybit's official MCP supplies market data, and a separate TypeScript MCP server controls every trade enabled action.
The architecture has two nonnegotiable rules:
- The model never receives direct access to Bybit's order creation tool.
- Every cycle begins by reading the strategy, previous decisions, trade history, and accumulated lessons.
Use the implementation prompt near the end to generate the project in an empty folder. Then review the generated code, configure Codex, run one manual testnet cycle, and only then enable scheduling.
The included EMA strategy is a teaching scaffold, not a demonstrated trading edge. Keep the system on testnet until its execution, logging, and safety behavior have been validated.
Architecture
The operating flow is:
Scheduler and controls
|
v
Codex with GPT-6 Astra
|
+---- READ ----> Bybit official MCP ----> market and account data
|
+---- WRITE ---> risk-gate MCP ----------> Bybit V5 REST
|
+---- six deterministic checks
+---- logs/audit.jsonl
+---- Telegram alertUse two separate MCP servers:
| Server | Responsibility | Credentials |
|---|---|---|
bybit | Prices, candles, funding, open interest, balances, positions, and order history | No key or a read only key |
risk-gate | Place orders, close positions, set leverage, and cancel orders | Trade enabled key with withdrawals disabled |
The model must not be given the official server's createOrder tool. All writes must pass through risk-gate.
1. Confirm the prerequisites
You need:
- Codex CLI 0.153.1 or later
- Access to the
gpt-6-astramodel - Node 20.6 or later
- A Bybit testnet account and testnet API credentials
- A Telegram bot token if you want alerts and remote commands
- A computer or VPS that remains awake while cron runs
Check the local tools:
node --version codex --version which npx
If Codex cannot find npx, use the absolute path returned by which npx in the MCP configuration.
2. Create the project structure
The completed project should use this layout:
trading-bot/
STRATEGY.md
HALT_TRADING
prompts/
cycle.md
memory/
trades.jsonl
learnings.md
decision.json
risk-gate/
src/index.ts
src/checks.ts
src/bybit.ts
dist/
scripts/
position-check.js
funding-check.js
telegram-alert.js
telegram-commands.js
logs/
audit.jsonl
cycle.logHALT_TRADING should exist only while trading is halted. The other empty files and directories can be created by the build prompt.
3. Configure Codex and the MCP servers
Add both servers to ~/.codex/config.toml:
model = "gpt-6-astra"
model_reasoning_effort = "high"
[mcp_servers.bybit]
command = "npx"
args = ["-y", "bybit-official-trading-server@latest"]
env = { BYBIT_TESTNET = "true" }
[mcp_servers.risk-gate]
command = "node"
args = ["/absolute/path/to/trading-bot/risk-gate/dist/index.js"]
env = { BYBIT_TESTNET = "true", BYBIT_API_KEY = "...", BYBIT_API_SECRET = "..." }Replace the risk gate path and credentials with your values. Keep credentials in environment configuration, never in the repository.
After changing this file, restart Codex. MCP servers connect when a session starts, so an existing session will not automatically load the new configuration.
Install the official Bybit MCP with:
codex mcp add bybit -- npx -y bybit-official-trading-server@latest
For USDT perpetuals, all Bybit MCP calls use:
{"category":"linear"}The primary read tools are:
| Tool | Purpose |
|---|---|
getTickers | Last price, mark price, funding, and volume |
getMarketKline | OHLCV candles |
getOrderbook | Bid and ask depth |
getFundingRateHistory | Historical funding |
getOpenInterest | Open interest trend |
getWalletBalance | Equity and available balance |
getPositionInfo | Current positions and unrealized PnL |
getOpenOrders | Active orders |
getOrderHistory | Historical orders |
getClosedPnl | Realized PnL |
4. Define one explicit strategy
Create `STRATEGY.md`. Keep every trading rule in this file so the model does not need to invent missing constraints.
# Strategy: EMA 9/21 trend follow, 4h ## Pairs BTCUSDT, ETHUSDT, SOLUSDT (category: linear) ## Timeframe Decide on 4h close. Manage on 15m. ## Entry Long: 9 EMA crosses above 21 EMA on 4h close, price above 200 EMA, funding < 0.03% per 8h. Short: mirror. No entry if a position is already open on that symbol. ## Exit Stop: 1.5 x ATR(14) from entry. Take profit: 3R, or 9/21 cross back, whichever first. Time stop: close if flat after 5 days. ## Sizing Risk 1% of equity per trade. qty = (equity * 0.01) / stop_distance. Leverage: BTC 5x, ETH and SOL 3x. Never above the risk gate cap. ## Execution Market entry only. Attach TP and SL on the order itself (takeProfit / stopLoss fields).
Replace this scaffold only after the complete pipeline works correctly on testnet.
5. Give the bot persistent file memory
Use three memory files with distinct responsibilities.
memory/trades.jsonl
Store one JSON object per closed trade:
{"ts":"2026-09-14T04:00:00Z","symbol":"BTCUSDT","side":"long","qty":0.05,"entry":63120,"exit":64890,"pnl_usdt":88.5,"r":1.9,"reason":"9/21 cross, funding neutral","outcome":"tp_hit"}memory/decision.json
Overwrite this file during every model cycle:
{
"ts": "2026-09-14T04:00:00Z",
"symbol": "BTCUSDT",
"action": "long",
"qty": 0.05,
"stop": 62100,
"take_profit": 66200,
"confidence": 0.7,
"reasoning": "4h 9/21 cross confirmed at close, price above 200 EMA, funding 0.01%. OI rising with price.",
"checked_learnings": [
"Sept 8: skipped SOL longs into funding > 0.05%, saved 1.2R"
]
}Writing the decision before placing an order creates an inspectable record of the intended action.
memory/learnings.md
Append one or two concise observations only after a trade closes. Each observation should state:
- What the bot expected
- What happened
- What it would change or continue checking
Cap this file at 50 lessons. When it reaches the cap, summarize the oldest 25 lessons into five entries. This prevents the model from accumulating an unlimited and potentially misleading history.
6. Install the cycle prompt
Create `prompts/cycle.md`:
You are the trading brain. Do exactly this, in order: 1. Read STRATEGY.md, memory/learnings.md, memory/decision.json, and the last 20 lines of memory/trades.jsonl. 2. For each pair in the strategy, call getTickers, getMarketKline (interval 240, limit 250) and getFundingRateHistory. Call getPositionInfo and getWalletBalance once. 3. Apply the strategy rules literally. Do not invent rules that are not in STRATEGY.md. 4. Write memory/decision.json with your decision and reasoning, including which learnings you checked. 5. If action is "long" or "short", call risk-gate place_order with the exact qty, stop and take_profit from your decision. If action is "close", call risk-gate close_position. 6. If the gate rejects the order, do not retry with different numbers. Log the rejection and stop. 7. Append one line to memory/learnings.md only if a trade closed since the last cycle. 8. Post a one-paragraph summary to Telegram via the alert script.
The rejection rule is essential. The model must not resize or alter an order to work around a failed safety check.
7. Implement the deterministic risk gate
The custom MCP server exposes only four write tools:
| Tool | Required behavior |
|---|---|
place_order(symbol, side, qty, stop, take_profit) | Check and place an entry with attached TP and SL |
close_position(symbol) | Read the current size and place an opposite side, full size, reduceOnly market order |
set_leverage(symbol, leverage) | Enforce the symbol leverage cap before forwarding |
cancel_order(symbol, order_id) | Log and forward the cancellation |
Run entry checks in this exact order:
- Reject all writes if
HALT_TRADINGexists. - Reject new entries when today's realized plus unrealized PnL is below negative 5 percent of equity measured at 00:00 UTC. Closures remain allowed.
- Reject an order when
qty * mark_priceexceeds 30 percent of equity. - Reject leverage above 10x for BTC or 5x for ETH and SOL.
- Reject a limit price more than 2 percent away from mark price.
- Reject a fourth open position or a second position on the same symbol.
The gate must fetch equity, PnL, mark price, leverage, and positions itself. It must never trust account values supplied by the model.
Use this check as the implementation reference:
async function checkOrder(req: OrderRequest): Promise<GateResult> {
if (fs.existsSync(path.join(BOT_DIR, "HALT_TRADING")))
return reject("kill_switch");
const equity = await bybit.equityUsdt();
const todayPnl = await bybit.pnlSinceMidnightUtc();
if (todayPnl < -0.05 * equity) return reject("daily_loss_cap");
const mark = await bybit.markPrice(req.symbol);
if (req.qty * mark > 0.30 * equity) return reject("notional_cap");
const lev = await bybit.leverage(req.symbol);
const cap = req.symbol === "BTCUSDT" ? 10 : 5;
if (lev > cap) return reject("leverage_cap");
if (req.price && Math.abs(req.price - mark) / mark > 0.02)
return reject("price_sanity");
const open = await bybit.openPositions();
if (open.length >= 3 || open.some(p => p.symbol === req.symbol))
return reject("position_cap");
return pass();
}Append every request to logs/audit.jsonl, whether it passes or fails. Each record should include:
- Timestamp
- Requested tool and arguments
- Result of every relevant check
- Approval or rejection reason
- Exchange response, if forwarded
The immediate kill switch is:
touch HALT_TRADING
Resume by removing the file:
rm HALT_TRADING
No service restart should be required.
8. Add scheduling and remote controls
Use cron for the three recurring jobs:
# entry scan on every 15m candle close */15 * * * * cd /path/to/trading-bot && codex exec --model gpt-6-astra "$(cat prompts/cycle.md)" >> logs/cycle.log 2>&1 # position check every minute * * * * * cd /path/to/trading-bot && node scripts/position-check.js >> logs/position.log 2>&1 # funding check every 4h 0 */4 * * * cd /path/to/trading-bot && node scripts/funding-check.js >> logs/funding.log 2>&1
Only the 15 minute job should invoke the model. The one minute position check and four hour funding check should be plain Node scripts.
Map Telegram commands as follows:
| Command | Action |
|---|---|
/status | Return equity, positions, today's PnL, and the last decision |
/halt | Create HALT_TRADING |
/resume | Remove HALT_TRADING |
/close BTCUSDT | Call the gate's close_position tool |
/why | Return the reasoning from memory/decision.json |
After each cycle, send one paragraph containing what was inspected, the decision, the reason, and the gate result. Send rejection and stop out alerts immediately.
9. Generate the complete project with Codex
Open an empty project folder and paste this prompt into Codex:
Build me an AI trading bot for Bybit USDT perpetuals following this architecture exactly. Runtime: this bot runs as scheduled `codex exec` calls plus a few plain Node scripts. The model is the brain; it may only trade through a risk-gate MCP server that you will build. It must never be given Bybit's createOrder tool directly. Build these pieces: 1. STRATEGY.md with an EMA 9/21 4h trend-follow rule on BTCUSDT, ETHUSDT, SOLUSDT, 1% risk per trade, ATR stop, 3R target, attached TP/SL. Mark it clearly as a teaching scaffold. 2. memory/ with trades.jsonl, learnings.md and decision.json, plus the schemas in comments. Cap learnings.md at 50 lessons with an auto-summarise rule. 3. prompts/cycle.md: the seven-step cycle prompt. Include the instruction that a gate rejection must never be retried with different numbers. 4. risk-gate/: a stdio MCP server in TypeScript exposing place_order, close_position, set_leverage, cancel_order. It holds BYBIT_API_KEY and BYBIT_API_SECRET from env, signs Bybit V5 REST requests (HMAC-SHA256), and switches between https://api-testnet.bybit.com and https://api.bybit.com on BYBIT_TESTNET. Before forwarding any write it runs six checks in order: kill switch file HALT_TRADING, daily loss cap 5% of equity since 00:00 UTC, notional cap 30% of equity, leverage cap BTC 10x / others 5x, price sanity 2% from mark, position cap 3 open and 1 per symbol. The gate fetches equity, mark price, leverage and positions itself; it never trusts numbers from the caller. Every call is appended to logs/audit.jsonl with request, check results and exchange response. close_position places a reduceOnly market order for the full position size. 5. scripts/: position-check.js (1m, alerts on stop or TP fill), funding-check.js (4h), telegram-alert.js (posts a paragraph), telegram-commands.js (polls for /status /halt /resume /close <symbol> /why). 6. A crontab.txt with the three schedules and a README that walks a beginner through: install Node, install Codex, add both MCP servers to ~/.codex/config.toml, create a testnet key, run the cycle once by hand, turn on cron, and the exact testnet-to-mainnet checklist. 7. Use Bybit's official MCP (npx -y bybit-official-trading-server@latest) for all reads. The real read tool names are getTickers, getMarketKline, getOrderbook, getFundingRateHistory, getOpenInterest, getWalletBalance, getPositionInfo, getOpenOrders, getOrderHistory, getClosedPnl. category is always "linear". Default everything to testnet. Do not write any key into any file. When you're done, run a dry cycle against testnet and show me the audit log line it produced.
After generation, inspect the project before running it. Confirm that no API credentials were written into source files and that the model cannot access an unrestricted order tool.
10. Validate on Bybit testnet
Bybit testnet is a separate site with separate accounts and credentials. A mainnet key used while BYBIT_TESTNET=true will fail authentication.
Follow this order:
- Create a testnet account and obtain test USDT.
- Create a testnet key with Contract trade permission only.
- Set
BYBIT_TESTNET=truefor both MCP servers. - Confirm that the risk gate holds the only trade enabled key.
- Restart Codex so it reconnects both MCP servers.
- Run one cycle manually:
cd /path/to/trading-bot codex exec "$(cat prompts/cycle.md)"
- Inspect
memory/decision.jsonbefore checking the resulting testnet order. - Inspect
logs/audit.jsonland confirm that every check was recorded. - Test the kill switch and verify that new writes are rejected.
- Test
close_positionand confirm that it sends a full sizereduceOnlyorder. - Enable cron only after the manual cycle behaves correctly.
- Leave the system on testnet for at least two weeks, reviewing
audit.jsonlandlearnings.mddaily.
Mainnet readiness checklist
Do not switch environments until every item is true:
- [ ] At least two weeks of clean testnet operation are complete.
- [ ] Every attempted write appears in
logs/audit.jsonl. - [ ] Gate rejections are never automatically retried with altered values.
- [ ] The kill switch rejects writes immediately.
- [ ] Position closures remain possible when the daily loss cap blocks entries.
- [ ] TP and SL are attached as expected.
- [ ] Position and leverage caps have been deliberately tested.
- [ ] Telegram halt, resume, close, status, and why commands work.
- [ ] The official Bybit order creation tool is not exposed to the model.
- [ ] No credentials exist in the repository or log files.
When ready, create a separate mainnet key with trade permission only, an IP allowlist, and withdrawals disabled. Save the secret when shown, set BYBIT_TESTNET=false, restart Codex, and begin with the smallest size permitted by the gate.