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

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

```bash
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:

```bash
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:

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

```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

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:

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

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:

```bash
touch HALT_TRADING
```

Resume by removing the file:

```bash
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:

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

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

```bash
cd /path/to/trading-bot
codex exec "$(cat prompts/cycle.md)"
```

7. Inspect `memory/decision.json` before checking the resulting testnet order.
8. Inspect `logs/audit.jsonl` and confirm that every check was recorded.
9. Test the kill switch and verify that new writes are rejected.
10. Test `close_position` and confirm that it sends a full size `reduceOnly` order.
11. Enable cron only after the manual cycle behaves correctly.
12. 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.