# Botxona — platform contract (reference)

This is the full technical contract a Telegram bot must satisfy to deploy cleanly on Botxona
(https://botxona.uz). It is the ground truth for the `botxona-prepare` and `botxona-fix` skills.
Everything below is verified against the platform source (`server/internal/validate`,
`server/internal/node`, `server/internal/control`). When in doubt, prefer this document over
assumptions — the validator (`bhvalidate` / the dashboard) is the final judge, not this text.

Always reply to the bot owner in plain Uzbek (unless they clearly write in another language) —
most Botxona users are not developers.

## 1. Upload limits (archive check)

A ZIP upload (or GitHub import) is rejected before any code is even read if it breaks these rules:

- Archive size ≤ 20 MB.
- ≤ 3000 files total.
- ≤ 100 MB unpacked total.
- ≤ 25 MB per single file.
- No path traversal (`..`, absolute paths) — rejected outright.
- Symlinks are silently dropped (not followed, not kept).
- Any executable/binary file (ELF, PE/`MZ`, Mach-O magic bytes, or a `.exe/.dll/.so/.dylib/.bin/.elf/.msi/.apk`
  extension) makes the whole upload **fail**.
- A `.env` file found anywhere in the archive is **auto-deleted** and reported as a warning — real
  secrets must never ship in the archive; they are entered in the Botxona dashboard instead
  (mirrored into `.env.example` as empty placeholders for documentation).
- These directories are skipped entirely on extraction (safe to leave in the repo, they just never
  reach the sandbox): `.git`, `node_modules`, `__pycache__`, `.venv`, `venv`, `.idea`, `.vscode`,
  `__MACOSX`.

## 2. `botxona.yaml` (optional, all keys optional)

```yaml
runtime: python        # python | node — overrides auto-detection
entry: main.py         # explicit entry file (Python) if there are several candidates
start: python -u bot.py  # explicit full start command — wins over entry/runtime detection
always_on: true         # true = never sleep; false = force sleep-on-idle even if a scheduler is detected
```

Only these four keys are read. Anything else is ignored. `.yml` extension also works. No key is
required — a clean `main.py` / `package.json "start"` project needs no `botxona.yaml` at all.

## 3. The 6 rules (checked by `internal/validate/rules.go`, exact check IDs)

Each becomes one line in the dashboard report with a status `pass` / `warn` / `fail`
(green / yellow / red overall). **Never rename or reinterpret these IDs** — the dashboard, the fix
prompts and `bhvalidate --json` all key off them.

1. **`rule1_token` — token only from `BOT_TOKEN`.**
   Fails if a literal Telegram token pattern (`\d{8,10}:[A-Za-z0-9_-]{35}`) appears anywhere in the
   source. Also fails if `BOT_TOKEN` is never referenced at all. The token is injected by the
   platform as the `BOT_TOKEN` environment variable at container start — never hardcode it, never
   commit it, never log it.

2. **`rule2_api` — Telegram API base only from `TELEGRAM_API_URL`.**
   The platform injects `TELEGRAM_API_URL` pointing at the node's own Telegram proxy
   (`http://<node-proxy>`, never `api.telegram.org` directly in production). The bot must build its
   HTTP client / session from this variable (with `https://api.telegram.org` only as the *default
   fallback value* when the variable is unset — a fallback on the same source line as the
   `TELEGRAM_API_URL` read is fine and does not fail the check). If `api.telegram.org` appears
   hardcoded anywhere else in the code, the check **warns** (if `TELEGRAM_API_URL` is also used) or
   **fails** (if it's the only address ever used). Analytics, rate limiting and the "long polling
   only" guarantee all depend on every call going through the proxy.

3. **`rule3_deps` — pinned dependencies.**
   Python: `requirements.txt` must exist, every line pinned (`==`, `>=~=<=<>`, or a `@ url/path`
   pin) — unpinned lines warn. Missing `requirements.txt` (with a `pyproject.toml` present) warns;
   missing entirely with real third-party imports detected fails.
   Node.js: `package.json` `dependencies` must not use `"*"`, `"latest"` or an empty version —
   warns otherwise. `package-lock.json` is recommended (soft, does not affect status).

4. **`rule4_entry` — explicit, unambiguous entry point.**
   Python: `botxona.yaml`'s `start`/`entry`, else exactly one of
   `main.py, bot.py, app.py, run.py, __main__.py, src/main.py, src/bot.py` must exist (more than
   one candidate warns and picks the first alphabetically; zero candidates fails).
   Node.js: `package.json` `scripts.start`, else `main`, else one of
   `index.js, bot.js, main.js, src/index.js, index.mjs` (warns if only found by convention).

5. **`rule5_data` — persistent data only under `DATA_DIR`.**
   The platform injects `DATA_DIR=/data` (a writable volume that survives redeploys and is backed
   up daily). The container's root filesystem is otherwise **read-only** (see §5 below) — writing
   anywhere else raises `OSError: Read-only file system` at runtime, not just a lint warning. The
   check passes if `DATA_DIR` is referenced; warns if the code clearly writes files (SQLite, JSON
   dumps, `open(..., "w")`, `fs.writeFile`, etc.) without ever reading `DATA_DIR`; passes silently
   if an external database (`DATABASE_URL`, `postgres://`, `mongodb://`, `REDIS_URL`) is used
   instead, or if the bot never persists anything.

6. **`rule6_env` — every setting documented in `.env.example`.**
   Every environment variable the code reads (except the platform-injected ones, see §7) should
   appear as a key in `.env.example` with a short comment. Missing file, or keys used in code but
   undocumented, both warn (not fail) — but a good `.env.example` is what lets a non-technical
   owner actually fill in the dashboard's settings form correctly, so treat every warning here as
   worth fixing.

## 4. Security scanner (separate from the 6 rules, group `security`)

Hard **fails** the build (source is never even attempted to build):
- Crypto-miner signatures (`xmrig`, `stratum+tcp`, `cryptonight`, `coinhive`, `minerd`, `nicehash`).
- Reverse-shell patterns (`/dev/tcp/`, `nc -e`, `bash -i >&`, raw socket + `os.dup2`).
- Obfuscated `exec`/`eval` of base64/zlib/marshal-decoded payloads.
- `curl | sh` / `wget | sh` style remote-script execution.
- DDoS tool signatures (`hping3`, `slowloris`, syn/udp flood).
- Any attempt to touch the Docker socket or `/proc/1/` (container breakout attempt).

Soft **warns** (build still proceeds, shown as yellow):
- SMTP on port 25 (`smtplib`, `nodemailer`, literal `:25`) — outbound port 25 is blocked at the
  network level; use a transactional email API (SendGrid, Mailgun, etc.) instead.
- Gambling-related keywords — flagged for manual review, not auto-rejected.

## 5. Auto-generated Dockerfile & sandbox (you never need to write this yourself)

Botxona generates the Dockerfile from the detected language:
- Python → `python:3.12-slim`, `pip install -r requirements.txt` (or `pip install .` for
  `pyproject.toml`), `PYTHONUNBUFFERED=1`.
- Node.js → `node:22-slim`, `npm ci`/`npm install --omit=dev` (runs `npm run build` first if a
  `build` script exists, then prunes dev deps).
- Both: runs as **non-root UID/GID `10001`**, `WORKDIR /app`, `ENV DATA_DIR=/data TZ=Asia/Tashkent`,
  the detected/declared start command as `CMD`.

At runtime the container is hardened regardless of language:
- `--read-only` root filesystem — only `/data` (the `DATA_DIR` volume) and `/tmp` are writable.
- `--cap-drop ALL`, `--security-opt no-new-privileges`, a `--pids-limit`.
- RAM and CPU are capped per the subscription's tariff (see §9) — exceeding RAM triggers an
  OOM-kill and a restart with backoff; 5 crashes within 10 minutes marks the bot `crashed` and
  stops auto-restarting (alerted to the owner).
- Network: by default (`NETWORK_MODE=isolated`) the bot container sees **only** the node's Telegram
  proxy — no internet, no other bots, no host. (`shared` mode, if the node operator enables it,
  allows outbound internet for bots that need a third-party API, still with inter-container
  communication disabled.)
- Optionally an extra syscall-level sandbox (gVisor/`runsc`) on nodes that have it installed — no
  code change needed either way.

## 6. Telegram access is proxy-only — long polling, never webhooks

This is the single most important behavioural rule and the one most "adapt my bot" jobs get wrong:

- The platform injects `TELEGRAM_API_URL` pointing at a per-node **Telegram Bot API proxy**. The
  bot must call this address for every Bot API method (never `api.telegram.org` directly).
- **The bot must use long polling (`getUpdates`) — it must never call `setWebhook`.** If it does,
  the proxy silently no-ops the call (`tgOK` with `ok:true`) and logs a `webhook_tried` event that
  Botxona surfaces back as a smoke-test warning — the bot will *look* like it succeeded but will
  never actually receive updates that way. Frameworks: use `start_polling` / `bot.start()` /
  `bot.launch()` — never start an HTTP server to receive Telegram updates, and never open a port to
  listen for webhook callbacks.
- `logout` and `close` Bot API methods are blocked by the proxy (`400 Bad Request`) — a bot doesn't
  own its own polling connection exclusively across restarts the way a self-hosted bot would;
  Botxona's node buffers `getUpdates` regardless of container restarts.
  `deleteWebhook(drop_pending_updates=true)` **is** honoured, but only on the very first start right
  after a fresh deploy — it will not discard updates that arrived because they woke the bot up.
- The proxy answers `getUpdates` from its own durable, ordered, at-least-once buffer per bot — this
  is what guarantees update ordering platform-wide even under webhook/poll/mock ingress on the
  control-plane side; the bot side just needs to keep calling `getUpdates` in a loop like it would
  against the real Telegram API.
- File downloads: use the same `TELEGRAM_API_URL` for `getFile`/file downloads
  (`{TELEGRAM_API_URL}/file/bot<token>/<path>`) — the proxy forwards these too.

## 7. Sleep / always-on

- A bot with no activity for `SLEEP_AFTER_SECONDS` (platform default 600s / 10 min) is put to sleep
  — its container is stopped, but it wakes up automatically (~1–3 seconds) the moment a new update
  arrives for it. This is normal and by design — it is how the platform fits thousands of bots on a
  small server. Don't try to "fix" it with keep-alive pings from outside; that just burns the
  owner's tariff quota for no benefit (the platform quietly no-ops most keep-alive traffic anyway
  since analytics/uptime is tracked by real user messages).
- If the bot legitimately needs a background loop — a scheduler (APScheduler, `node-cron`,
  `setInterval`, `JobQueue`, etc.) — the validator detects this automatically and marks it
  `always_on` (a scheduler cannot run while the container is asleep). `botxona.yaml`'s `always_on:
  true|false` always overrides the auto-detection either way.
- Always-on is otherwise a tariff feature (see §9) — on lower tariffs the owner picks `auto | on |
  off` in **Sozlamalar → Ish rejimi** in the dashboard; `auto` sleeps on idle like above.

## 8. Backups

Every bot's `/data` (its `DATA_DIR`) is snapshotted daily (03:00 Tashkent time), encrypted
(AES-256-GCM, a key derived per-bot from the platform's master key — the bot never sees or handles
this), and kept for 7/14/30 days depending on tariff. Restores (including an automatic pre-restore
safety snapshot) and signed 24h-expiry downloads are one click in the dashboard. None of this
requires anything from the bot's code beyond rule 5 — write everything you want kept under
`DATA_DIR` and it's covered automatically.

## 9. Smoke test (what happens right after a build, before "live")

After a successful `docker build`, Botxona runs the new image in a **throwaway container** for
`SMOKE_SECONDS` (default 30s) before promoting it to live traffic:
- `DATA_DIR` is a disposable tmpfs during smoke — nothing written here is real yet, and nothing
  from before is visible either (don't be surprised if a "first run" migration re-executes).
  No real Telegram updates are delivered during smoke — `getUpdates` just idles.
- Watched: does the process stay running the whole window (a crash or non-zero exit within the
  window fails smoke and blocks promotion); does it call `setWebhook` (flagged, see §6); does it
  hit `401`/`404` against the proxy repeatedly (usually an invalid/placeholder token — flagged);
  memory/CPU behaviour.
- Only after smoke passes is the new container promoted to `live_deployment_id` and given real
  traffic; the previous container (if any) is replaced only at that point, so a bad deploy never
  takes down a working bot.

## 10. Tariffs (`ram_mb` / `cpu` are hard container caps)

| Tariff | RAM | CPU | Backups | Bots | Always-on | GitHub auto-deploy | Full analytics |
|---|---|---|---|---|---|---|---|
| Start | 256 MB | 0.25 vCPU | 7 days | 1 | no (auto-sleep) | no | no |
| Pro | 512 MB | 0.5 vCPU | 14 days | 1 | optional | yes | yes |
| Business | 1024 MB | 1.0 vCPU | 30 days | 1 | always | yes | yes |
| Freelancer | 256 MB (x5) | 0.25 vCPU each | 7 days | 5 | no | no | no |

A bot that needs more than ~200 MB RAM at idle (e.g. heavy ML/image libraries) will get OOM-killed
on Start — recommend Pro/Business instead of trying to work around the cap in code.

## 11. Platform-injected environment variables (never put these in `.env.example` as user-editable)

These are set by Botxona automatically at container start — code should read them, never write
sample "real" values for them into `.env.example` beyond an empty placeholder / comment:

- `BOT_TOKEN` — the bot's live token (from BotFather, entered once in the dashboard).
- `TELEGRAM_API_URL` — this node's Telegram proxy base URL.
- `DATA_DIR` — always `/data` in production (locally, default to `./data` in code for convenience).
- `TZ` — always `Asia/Tashkent`.
- `PORT` — reserved; bots normally don't need to listen on a port at all (no inbound HTTP/webhook
  server — see §6). Never bind to port 25 (SMTP, blocked).

Everything else the bot needs (admin Telegram IDs, feature flags, business-specific config, a
third-party API key, etc.) must be documented in `.env.example` and is entered by the owner in the
dashboard's settings form (rule 6).
