Miles Free AI Library
← Back to resources
BlueprintInstagramSep 23, 2026

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

Get the full MD

Download the complete Bybit trading bot guide

Free
Download MD

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:

  1. The model never receives direct access to Bybit's order creation tool.
  2. 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:

TEXT
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 alert

Use two separate MCP servers:

ServerResponsibilityCredentials
bybitPrices, candles, funding, open interest, balances, positions, and order historyNo key or a read only key
risk-gatePlace orders, close positions, set leverage, and cancel ordersTrade 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-astra model
  • 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:

Shell
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:

TEXT
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.log

HALT_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:

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:

Shell
codex mcp add bybit -- npx -y bybit-official-trading-server@latest

For USDT perpetuals, all Bybit MCP calls use:

JSON
{"category":"linear"}

The primary read tools are:

ToolPurpose
getTickersLast price, mark price, funding, and volume
getMarketKlineOHLCV candles
getOrderbookBid and ask depth
getFundingRateHistoryHistorical funding
getOpenInterestOpen interest trend
getWalletBalanceEquity and available balance
getPositionInfoCurrent positions and unrealized PnL
getOpenOrdersActive orders
getOrderHistoryHistorical orders
getClosedPnlRealized PnL

4. Define one explicit strategy

Copy
Create `STRATEGY.md`. Keep every trading rule in this file so the model does not need to invent missing constraints.
MARKDOWN
# 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:

JSON
{"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:

JSON
{
  "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

Copy
Create `prompts/cycle.md`:
TEXT
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:

ToolRequired 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:

  1. Reject all writes if HALT_TRADING exists.
  2. 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.
  3. Reject an order when qty * mark_price exceeds 30 percent of equity.
  4. Reject leverage above 10x for BTC or 5x for ETH and SOL.
  5. Reject a limit price more than 2 percent away from mark price.
  6. 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:

TYPESCRIPT
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:

Shell
touch HALT_TRADING

Resume by removing the file:

Shell
rm HALT_TRADING

No service restart should be required.

8. Add scheduling and remote controls

Use cron for the three recurring jobs:

CRON
# 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:

CommandAction
/statusReturn equity, positions, today's PnL, and the last decision
/haltCreate HALT_TRADING
/resumeRemove HALT_TRADING
/close BTCUSDTCall the gate's close_position tool
/whyReturn 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:

TEXT
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:

  1. Create a testnet account and obtain test USDT.
  2. Create a testnet key with Contract trade permission only.
  3. Set BYBIT_TESTNET=true for both MCP servers.
  4. Confirm that the risk gate holds the only trade enabled key.
  5. Restart Codex so it reconnects both MCP servers.
  6. Run one cycle manually:
Shell
cd /path/to/trading-bot
codex exec "$(cat prompts/cycle.md)"
  1. Inspect memory/decision.json before checking the resulting testnet order.
  2. Inspect logs/audit.jsonl and confirm that every check was recorded.
  3. Test the kill switch and verify that new writes are rejected.
  4. Test close_position and confirm that it sends a full size reduceOnly order.
  5. Enable cron only after the manual cycle behaves correctly.
  6. Leave the system on testnet for at least two weeks, reviewing audit.jsonl and learnings.md daily.

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.

Continue exploring

What to watch next