# Raid bot

`/raid` posts a board that tracks a tweet's likes, retweets and replies against a goal, editing itself until the goal is met or the raid is stopped.

## Starting a raid

> **Beta**
>
> The raid bot is currently in beta.

`/raid` is the only raid command. There is no stop, status or leaderboard command — a raid runs until it meets an end condition (see **The raid board**) or is replaced by a new `/raid` in the same chat.

The command is case-insensitive and accepts the `@botname` form: `/raid`, `/RAID`, `/Raid@yourbotusername` all match. The leading slash is required — unlike some other bot commands, a bare `raid …` with no slash does not trigger it.

#### Command matching

| Setting | Value |
| --- | --- |
| Pattern | `/raid`, optionally followed by `@<botusername>`, then either the end of the message or a space. |
| Case | Insensitive — `/RAID`, `/Raid`, `/raid` all match. |
| Chat types | Groups, supergroups and private chats — see **Who can start a raid** for what's authorized where. |

#### Start a raid in a group

1. Turn on Raid Mode for the bot, if it isn't already — dashboard Raid Bot tab (or the admin Raid tab), power button.
2. Send `/raid <tweet link> <n>` or `/raid <tweet link> <likes> <reposts> <replies>` in the group.
3. The bot deletes your `/raid` message and posts the board in its place. The board is never a reply, so it isn't tied to your original message.

*Usage reply — sent when the command can't be parsed*

```
Usage: /raid <tweet link> <n>
   or: /raid <tweet link> <likes> <reposts> <replies>
Example: /raid https://x.com/user/status/123… 5
```

#### Argument parsing

Tokens after `/raid` (or `/raid@botname`) can appear in any order. Each one must be either the tweet link or a whole number; the first token that is neither aborts the whole command and triggers the usage reply above.

*parse\_raid\_args*

| Rule | Detail |
| --- | --- |
| Tweet link | May appear anywhere among the arguments, not only first. Only the first matching link counts — a second link token is rejected. |
| Numbers | Each numeric token must be all digits and at most 6 digits long. A 7-digit number (for example `1000000`) is rejected as an unparseable token — it never reaches the goal-range check below. |
| How many numbers | Exactly 1 or exactly 3. One number applies to all three metrics (likes, retweets, replies). Three numbers map in order to likes, retweets/reposts, replies. |
| Anything else | Any other token — extra words, a malformed link, 2 numbers, 4+ numbers — aborts the whole command. Usage reply, nothing started. |

*Reply when every goal is 0, or a goal exceeds 100000*

```
Goals must be 0–100000 with at least one above 0.
```

The usage text calls the second metric "reposts"; the board and its configuration call the same metric "retweets". Same value, two names.

#### Accepted tweet links

| Part | Accepted |
| --- | --- |
| Scheme | Optional — with or without `http://` / `https://`. |
| Subdomain | Optional — bare, `www.`, or `mobile.`. |
| Host | `x.com`, `twitter.com`, `fxtwitter.com`, `vxtwitter.com`, `fixupx.com`, `twittr.com`, `nitter.net` — case-insensitive. |
| Path | `/<handle>/status/<id>` or `/<handle>/statuses/<id>`. Handle is 1–15 word characters; id is digits. |
| Extra text | The link can sit inside a longer token, such as a trailing `?s=20` — it only needs to match somewhere inside the token. |
| Output | Whichever form you paste, the board always shows the canonical `https://x.com/<handle>/status/<id>` link. |

## Who can start a raid

`/raid` carries its own authorization check. The router applies no restriction of its own before it (its route scope is "any" — no gate), and the raid route does nothing but call the raid handler, so nothing upstream of the checks below can block a `/raid`.

*is\_authorized — any one branch passing is enough*

| Check | Passes when | Chat type |
| --- | --- | --- |
| Owner or admin | The sender is the bot's pinned owner (by user id), or their username matches the owner username, or their username is on the bot's admin list. | any |
| DM-authorized username | The sender's username is on the bot's DM-authorized usernames list. | private chat only |
| Allowed chat | The chat's id is one of the bot's allowed groups (or its admin groups). | group / supergroup — matches on chat id alone |

In an allowed group, the third check passes for the chat itself, not for a specific person. **Every member of that group can start a raid.** There is no per-user check and no requirement to be a Telegram admin of the group.

In a private chat, the chat id is the sender's own DM id, which is never in a group's allow-list — so only the first two checks (owner/admin, or a listed DM-authorized username) can pass in DMs.

#### Raid Mode and private chats

Raid Mode (`raid_enabled`) is a single on/off switch for the whole bot, default off. **It is only checked in group and supergroup chats.** A private chat is exempt from the check entirely — an authorized user (owner/admin, or a listed DM-authorized username) can run `/raid` by DM even while Raid Mode is switched off for the bot.

Raid Mode has no `/menu` control in Telegram. Turn it on or off from the dashboard — the Raid Bot tab's power button, or the admin Raid tab's per-bot toggle.

> **Unauthorized and feature-off are silent**
>
> If `/raid` is sent by someone `is_authorized` rejects, or in a group where Raid Mode is off, the bot sends no reply, does not delete the message, and gives no acknowledgement of any kind. The only sign a raid didn't start is that no board appears.

## Goals, baseline and completion

**A raid's goals are gains above a baseline, never absolute counts.** Send `/raid <link> 5` and the raid ends once each raided metric has risen by 5 from wherever it stood when the raid started — not once it reaches 5.

#### How the numbers are computed

| Setting | Value |
| --- | --- |
| Baseline (`base`) | The tweet's likes/retweets/replies counts from the first successful fetch, taken before the board is sent. |
| Current (`cur`) | The absolute counts from the most recent successful poll — polled once per second while the raid runs. |
| Gain | `max(0, cur - base)` per metric. Floored at 0, so a metric that drops below its baseline (an unlike, for example) never shows negative progress. |
| A goal of 0 | Means "don't raid this metric." Its row is hidden from the default board layout and its per-metric tokens render empty — see the token table. |
| Completion | Every metric's raw `cur - base` must be at least its goal, including metrics with goal 0 — those must simply not have dropped below baseline. At least one goal must be above 0 to start a raid at all. |

> **The intro line shows an absolute number, not the goal**
>
> The board's opening line reads "Raid going until the tweet has 20 likes, …" — an absolute count (baseline + goal) — even though the underlying goal is a delta. `/raid <link> 5` on a tweet that already has 15 likes prints "until the tweet has 20 likes," not "5 more likes." The goal is still 5: a gain of 5 above whatever the tweet had when the raid started.

Because completion checks raw `cur - base` for all three metrics, not only the ones being raided, a metric you set to 0 still has to not fall below its baseline for the raid to complete. If it does, that metric's completion condition is never satisfied — see **The raid board** for what happens to a raid that never completes on its own.

## How participation is counted

**Participation in a raid is not counted or verified.** The board reads the tweet's public like/retweet/reply counters directly. It has no list of who took part and no way to check that any specific person did anything.

* The polling loop fetches only the tweet's three aggregate counters — nothing about who liked, retweeted or replied.
* The board's renderer is a pure function of the goals, baseline and current counts. It has no access to any user list, and nothing in the running raid loop reads a user id, username, or sender identity — only `is_authorized`, at the moment `/raid` is sent, looks at who sent the command.
* Any rise in the tweet's counters counts toward the goal, regardless of where it came from — group members, outsiders, or otherwise.
* There is no per-user scoreboard, no proof-of-like, and no persistence of raid history. The list of currently-running raids lives in memory only and is cleared when the bot process restarts.

## The raid board

The board is a single message the bot posts, pins, and keeps editing until the raid ends. It is never a reply — the `/raid` command that started it is deleted the moment the raid is accepted, so the board is the only message holding the tweet link.

| Behavior | Detail |
| --- | --- |
| Send | Standalone message — `sendPhoto` with the board as the caption if a raid image is set, otherwise `sendMessage` with link previews disabled. |
| Pin | Pinned silently on send. Telegram's own "pinned a message" service line is deleted automatically when it announces one of the bot's own pins. |
| Reposition | Every 120 seconds while the raid runs, the board is reposted at the bottom of the chat: send the fresh copy, pin it, then delete the old one. A failed repost keeps the old board and retries after 10 seconds. |
| Edit | Between repositions, the board is edited in place — but only when the rendered text actually changed, and at least 3 seconds have passed since the last edit. |
| "Not modified" errors | Swallowed silently — not treated as a failure. |
| End | Edited in place to its end line, then unpinned. |

#### Metrics tracked

Exactly three metrics are tracked: likes, retweets, replies. A metric with goal 0 is skipped — hidden from the default board layout, and blank wherever its tokens appear in a custom template.

#### Photo vs. text mode

| Mode | Trigger | Length cap |
| --- | --- | --- |
| Photo | A raid image is set (dashboard editor's image slot). | 1024 visible characters, as the photo caption. |
| Text | No raid image set — or a photo send failed, in which case the bot falls back to a text message for that send. | 4096 visible characters. |

Length is measured as visible characters with HTML tags stripped, counted as UTF-16 units. Going over the cap drops whole trailing lines from the board — never a mid-tag cut.

#### Default layout

When the board has no custom template (`boardHtml` unset), the bot builds the layout itself. Shown below with the dashboard editor's own preview numbers — a 20/18/23 target, progress at 2 of 5 likes, 0 of 5 retweets, 5 of 5 replies:

*Default board layout (no custom template)*

```
Raid going until the tweet has 20 likes, 18 retweets and 23 replies:

<progress bar>

🟥 Likes: 2 of 5
🟥 Retweets: 0 of 5
✅ Replies: 5 of 5

https://x.com/…/status/…
```

A custom template is responsible for its own tweet link: the bot only auto-appends the tweet URL below the board when there is no custom template. Leave `{tweet}` out of a custom template and the link never shows.

#### How a raid ends

| Trigger | What happens |
| --- | --- |
| Every goal met | Board edits in place to the Complete end line, then unpins. |
| 30 consecutive failed fetches | Board edits to the Stopped end line. Counts any failed poll in a row — a network error, a non-JSON response, or a bad result from the tweet lookup — with no successful poll in between. |
| Replaced | A new `/raid` sent in a chat that already has one running cancels the running raid — no reply of its own. Its board closes out through the same end line the dashboard editor's "Over" view configures. |
| Bot restart | Every raid running in the bot process is cancelled and its board closed out the same way as a replaced raid. |

There is no time limit on a raid by itself — it keeps polling and editing until one of the triggers above fires.

#### Default end lines

| Setting | Value |
| --- | --- |
| Complete (every goal met) | ✅ **Raid complete!** Every target hit — GG. |
| Over (raid replaced) | ⏰ **Raid over.** Hit \{got}/\{total}. |
| Stopped (30 failed fetches) | ⚠️ **Raid stopped** — couldn't reach the tweet. \{got}/\{total} counted. |

## Board template tokens

The default layout above is what the bot builds on its own. Setting a board template (`boardHtml`, edited in the dashboard's board editor) replaces it — the template is plain text and HTML with `{token}` placeholders, substituted one line at a time.

Tokens match `{` + lowercase letters/underscores + `}` only. `{Likes}` or `{likes1}` are not recognized as tokens and pass through as literal text — as does any `{token}` whose name isn't in the table below.

*Every recognized board token*

| Token(s) | Renders to | Empty when |
| --- | --- | --- |
| `{intro}` | The intro line while the raid is running; the end line once it's complete, over, or stopped. | never |
| `{bar}` | The rendered progress bar for the selected design. | never |
| `{got}` | Total progress across active metrics — each metric's gain capped at its own goal, then summed. | never |
| `{total}` | Sum of all active goals (minimum 1). | never |
| `{targets}` | The joined target phrase, for example "20 likes, 18 retweets and 23 replies." | no metric has a goal above 0 |
| `{footer}` | The footer text, if set. | footer not set |
| `{tweet}` | The canonical tweet link. | never in practice |
| `{likes_status}` `{retweets_status}` `{replies_status}` | Done or pending status emoji for that row. | that metric's goal is 0 |
| `{likes_count}` `{retweets_count}` `{replies_count}` | "\<progress> of \<goal>" for that row, progress capped at the goal. | that metric's goal is 0 |
| `{likes_got}` `{retweets_got}` `{replies_got}` | The capped progress alone, as a bare number. | that metric's goal is 0 |
| `{likes_goal}` `{retweets_goal}` `{replies_goal}` | The goal alone, as a bare number. | that metric's goal is 0 |
| `{likes_label}` `{retweets_label}` `{replies_label}` | That row's label (default "Likes:", "Retweets:", "Replies:"). | that metric's goal is 0 |
| `{likes}` `{retweets}` `{replies}` | The absolute target for that metric — baseline + goal. Populated for all three metrics regardless of whether they're being raided. | never |

> **A bare \{likes}/\{retweets}/\{replies} line never drops**
>
> These three tokens are always populated, even for a metric with goal 0 — so a line whose only token is one of them survives the line-drop rule below no matter what.

#### The line-drop rule

Applied per line — the template is split on newlines — before the board is sent or edited:

1. Find every known token on the line. A token counts as "known" if it's one of the names in the table above.
2. Substitute tokens with their values. Text inside an HTML tag (`<...>`) is passed through untouched — a token inside a tag's attributes is never substituted.
3. Drop the whole line if all three hold: it has at least one known token; every distinct known token on it resolved to an empty value; and the rendered line does not contain `<a ` — a link tag keeps the line alive even if its tokens are empty.
4. If a line was just dropped and the next line is blank, that blank line is dropped too — but only if the line before it was already blank, or nothing has been kept yet. This avoids leaving a double gap where a row was removed.

A line mixing an empty token with a non-empty one survives — every known token on the line has to be empty for it to drop. A line of plain text with no tokens at all never drops.

#### Length limit

| Setting | Value |
| --- | --- |
| Photo caption | 1024 visible characters (UTF-16 units, HTML tags stripped). |
| Text message | 4096 visible characters, same measurement. |
| Over the limit | Whole trailing lines are dropped, never a mid-tag cut. If even the first line alone is over the limit, the text is sent unchanged. |

#### Worked example — default board template

| Input | Output |
| --- | --- |
| \{intro} | `Raid going until the tweet has **20** likes, **18** retweets and **23** replies:` |
| \{likes\_status} Likes: \{likes\_count} | `🟥 Likes: **2 of 5**` |
| \{retweets\_status} Retweets: \{retweets\_count} | `🟥 Retweets: **0 of 5**` |
| \{replies\_status} Replies: \{replies\_count} | `✅ Replies: **5 of 5**` |
| \{tweet} | `https://x.com/…/status/…` |

*`{bar}` is left out of this example — its rendered cells depend on the bar design chosen in the editor. Status emoji shown (🟥 pending, ✅ done) are the built-in defaults; a bot can replace either with a premium emoji.*

## The dashboard editor

Raid boards, images, end-state text and the progress bar are all set from the dashboard, not from Telegram. Raid mode is a Telegram-only feature — using it against a non-Telegram bot fails with "Raid mode is only available for Telegram bots."

#### Entry points

| Surface | Where |
| --- | --- |
| User dashboard | Beta tab → Raid Bot. Telegram bots only. |
| Admin | Raid tab, alongside the other per-bot feature lists. |

The user dashboard's Raid Bot tab reads: "The board @\<botusername> posts and keeps updating for the duration of a raid — /raid \<tweet link> \<n>" (the bot's display name in place of the username, if it has one), with an **Edit** button labeled "Raid Bot Appearance" that opens the editor.

The admin Raid tab lists every bot with raid configured or turned on, tagged **customized** or **default message**, with a per-row on/off toggle. Toggling shows "Raid on for @\<user>" or "Raid off for @\<user>", with " — bot restarted" appended when the bot process actually restarted.

#### The editor

The editor opens as a modal titled "Edit Raid Message" — the same reminder from the usage reply is shown above the board: `/raid https://x.com/…/status/… 5`. In the user dashboard it also carries a power button, "Raid mode on" / "Raid mode off", that flips Raid Mode for the bot and restarts it; the admin editor does not show this button.

#### The four views

| View | Label | Edits |
| --- | --- | --- |
| live | Live | The intro line (`introHtml`). |
| complete | Complete | The Complete end line (`completeHtml`), prefixed by its own status emoji. |
| expired | Over | The Over end line (`expiredHtml`), prefixed by its own status emoji. |
| error | Stopped | The Stopped end line (`errorHtml`), prefixed by its own status emoji. |

Only the first line of the board changes between views — the bar and the metric rows are shared across all four.

There is no view or mode named "Layout." Reset layout (below) is a one-shot action, not a fifth view.

#### Click-to-edit

Clicking a rendered token in the live preview opens a popover for it:

| Click target | Popover | Controls |
| --- | --- | --- |
| `{bar}` | "Raid progress bar" — a list of bar designs. | Row 1 is "follows Edit bar" (inherit the design used elsewhere in the editor), then house presets (tagged "default"), your own catalog ("Yours"), and mintable library designs ("Library · \<count>", tagged "mints on save" until minted). |
| A status emoji (`{likes_status}`, `{end_status}`, etc.) | Titled "\<Metric> · still going", "\<Metric> · target hit", "Raid complete", "Raid over" or "Raid stopped." | "Choose from created" (your premium emoji), "+ New" (opens the emoji designer), a premium-ID paste field, a plain-emoji field (16-character max, placeholder = the slot's default), and a reset link. Hint: "Premium ID wins; the plain emoji is what non-premium clients see." |
| Any other emoji in the line | "Line emoji." | Swaps that one inline emoji. Hint: "Swaps just this emoji — it stays normal text, delete it like any character." |

#### Reset layout

The "Reset layout" pill (tooltip: "Bring back any deleted template line — your emojis and custom text stay") restores any deleted line from the built-in template, in template order and spacing. Lines you added keep their position relative to the template line below them; a restored line reuses an adjacent icon-only line instead of duplicating a row under a stranded emoji. Emptying the intro or an end-state line back to nothing reverts that slot to its default.

The same line-matching mechanic backs Reset layout in the commands, X-automations and buy-bot editors.

#### Image

Click the image slot to upload a raid image — jpg, jpeg, png or webp, up to 5MB — and the board sends as a photo with the board as its caption. Click the X on an uploaded image to remove it and fall back to a plain text board.

An unsupported file type fails with "Unsupported type .\<ext> — the raid board sends as a photo, use jpg/png/webp." An oversized file fails with "File too large — Telegram caps URL-fetched photos at 5MB." A successful upload shows "Image set — raids now send it above the board"; a failed removal shows "Couldn't remove the image."

#### Save

**Save** is disabled until something changes, and reads "Saving…" while in flight. On success: "Saved — pushed to the bot."

Metric labels are read back out of the board body itself — whatever text sits next to that row's status emoji and count token, after both are stripped out. A label longer than 120 characters after stripping is discarded to empty and falls back to the default label, rather than being saved.

> **Note**
>
> Saving from the editor always writes a concrete board template, even if nothing else changed. From that point the bot renders from the stored template rather than its own built-in default.

If the selected bar design is an unminted library design, the server starts minting and the editor polls status every 3 seconds for up to 12 minutes. The status line reads “Minting “\<name>” — \<done>/\<total> emoji · \<stage>.” Past 12 minutes: "Minting is taking longer than 12 minutes — Telegram flood-wait. Save again later; the set finishes on its own."

## Limits

*Command limits*

| Limit | Value |
| --- | --- |
| Goal maximum | 100,000 per metric |
| Goal minimum | At least one metric above 0 |
| Numeric token length | Up to 6 digits — a 7-digit token is rejected as unparseable, not as out of range |
| Numbers per command | Exactly 1 or exactly 3 |
| Tweet handle length | 1–15 word characters |
| Concurrent raids, whole bot | 8 — refused for a new chat past this cap; a chat that already owns a raid can still replace it even at the cap |
| Raids per chat | 1 — a new `/raid` replaces the running one |
| Consecutive failed fetches before giving up | 30 |
| Poll interval | 1 second |
| Minimum gap between edits | 3 seconds, and only if the text changed |
| Board reposition interval | 120 seconds |
| Reposition retry interval | 10 seconds, on a failed repost |

#### Busy reply

| Setting | Value |
| --- | --- |
| Bot already running 8 raids, this chat has none yet | "Too many raids running right now — try again soon." No raid starts. |

*Message-size limits*

| Limit | Value |
| --- | --- |
| Photo caption | 1024 visible characters |
| Text message | 4096 visible characters |
| Over the limit | Whole trailing lines dropped |

*Dashboard editor limits*

| Field | Cap |
| --- | --- |
| Intro / footer / end-state text | 700 visible characters |
| Board template (`boardHtml`) | 1000 visible characters |
| Metric label | 120 visible characters |
| Plain emoji | 16 characters |
| Premium emoji ID | 5–32 digits |
| Raid image | jpg, jpeg, png or webp, up to 5MB |
| Allowed HTML tags | `b i u s code pre blockquote tg-spoiler tg-emoji a` |
| Link scheme | `https://` or `tg://` only |
| Progress-bar catalog | 24 designs per bot |
| Mint wait | Up to 12 minutes, polled every 3 seconds |

> **A board can be longer than a photo caption allows**
>
> The board template field accepts up to 1000 visible characters, but a photo-mode board is capped at 1024 visible characters total once the intro, bar and tweet link are all substituted in. A board built close to the 1000-character ceiling can exceed 1024 once rendered — the overflow is dropped a whole line at a time from the bottom, silently.
