Message layout
Every buy post — the default layout and any custom body — is built from `{PLACEHOLDER}` tokens inside header, body and row runs. Twenty tokens resolve to…
Message layout
Every buy post — the default layout and any custom body — is built from {PLACEHOLDER} tokens inside header, body and row runs. Twenty tokens resolve to text; eleven of them can also serve as a run's own link target.
Text placeholders
| Token | What it renders | Notes |
|---|---|---|
{AMOUNT} | Native amount spent, quantity-formatted. | Always renders, even for a zero-value buy — never empty, so it can't trigger a drop by itself. |
{NATIVE} | Native token symbol of the chain. | Identity placeholder (IDENTITY_PH) — never decides a drop. |
{USD} | The buy amount in USD, 2 decimal places. | Always renders ($0.00 at minimum) — never empty. |
{TOKENS} | Tokens received, quantity-formatted. | Always renders — never empty. |
{TOKEN} | Token symbol. | Identity placeholder — never decides a drop. See Case matters, below. |
{token} | Token name — the long name. | Identity placeholder — never decides a drop. See Case matters, below. |
{PRICE} | Formatted USD price per token. | Always renders — a non-positive price literally renders $?, never empty. |
{MCAP} | Market cap in USD, 0 decimal places. | Always renders ($0 at minimum) — never empty. |
{WALLET} | Shortened buyer wallet — first 6 characters + last 4, past 12 characters. | Linked to the address explorer when walletLinkOn is on. Empty only if the buy carries no wallet address at all — can drop its line. |
{POSITION} | New!, or a +-prefixed percentage change (1 decimal place). | Empty only when the pre-buy balance couldn't be read — the one case where it can drop its line. |
{EMOJIS} | The size row — the size emoji repeated N times. See Size-emoji scaling, below. | Identity placeholder — never decides a drop. Raw HTML, never escaped. |
{TX} | The literal word Tx, linked to the transaction explorer. | Empty when txLinkOn is off or no tx explorer base resolves — can drop its line. |
{CHART} | The configured chart label, linked to the resolved chart URL. | Empty when no chart resolves — can drop its line. |
{CA} | The contract address — the config's own, else the market's base mint. | Empty only if neither one resolves (a live buy bot always has a contract address) — can drop its line. |
{DEX} | The DEX name, hyphens replaced with spaces, Title Cased. | Empty when the market carries no DEX name — can drop its line. |
{HOLDS} | The buyer's token balance after this buy. | Empty when the pre-buy balance couldn't be read — can drop its line. |
{LIQ} | Liquidity in USD, 0 decimal places. | Empty when ≤ 0 — can drop its line. |
{VOL24} | 24-hour volume in USD, 0 decimal places. | Empty when ≤ 0 — can drop its line. |
{BUYS24} | 24-hour buy count, comma-grouped. | Empty when 0 — can drop its line. |
{HOLDERS} | Holder count, comma-grouped. | Empty unless a positive integer — can drop its line. |
Note
Quantity fields —
{AMOUNT},{TOKENS},{HOLDS}— share one formatter (_fmt_qty): comma-grouped with 0 decimals at 1,000 or over; 2 decimals with trailing zeros stripped from 1 up to 1,000; 4 decimals with trailing zeros stripped below 1.
Note
{TOKEN},{token},{NATIVE}and{EMOJIS}are the four identity placeholders (IDENTITY_PH). The code's own comment on that set: "always filled; never decide a line drop."
Case matters: {token} vs {TOKEN}
Note
Placeholder case is meaningful.
{TOKEN}(uppercase) renders the token's symbol.{token}(lowercase) renders the token's name — the long name.token/TOKENis the only placeholder that exists in both cases; every other placeholder is uppercase-only.
Case rule, rendered
| Input | Output |
|---|---|
| Template line | {TOKENS} {TOKEN} vs {token} |
| Rendered | 1,000 NOT vs Notcoin |
URL-slot tokens
11 of the 20 placeholders can also fill a run's own link target directly.
| Token | Links to | When unset |
|---|---|---|
{TX} | The transaction, on the chain's tx explorer. | Renders empty — no link, no text — when txLinkOn is off or no tx explorer base resolves. |
{CHART} | The resolved chart URL for the configured chartProvider. | Empty when no chart resolves. |
{WALLET} | The buyer wallet, on the chain's address explorer. | Text still renders; just unlinked, when walletLinkOn is off. |
{WEBSITE} | The Website link slot. | Plain text, not a dead link. |
{TWITTER} | The X link slot (/buybotx, /buybottwitter). Where the bot auto-labels this slot — the default layout's tail link — the label is X, never "Twitter" (SITE_LINK_LABEL). | Plain text, not a dead link. |
{TELEGRAM} | The Telegram link slot. | Plain text, not a dead link. |
{DISCORD} | The Discord link slot. | Plain text, not a dead link. |
{DOCS} | The Docs link slot. | Plain text, not a dead link. |
{MEDIUM} | The Medium link slot. | Plain text, not a dead link. |
{REDDIT} | The Reddit link slot. | Plain text, not a dead link. |
{GITHUB} | The GitHub link slot. | Plain text, not a dead link. |
Note
An unset site-link slot renders as plain text, never a dead link — the run keeps its label, it just isn't wrapped in a link.
The default layout (no custom body)
With no custom body set, the bot renders a fixed structure instead of running the line-drop rule at all: a header, the emoji row, a blank line, six body rows in a fixed order, then a blank line and a tail of links.
- Header — the owner's
headerPartsif set, else<Name> Buy!, the name linked to the chart when one resolves. - The emoji row (
{EMOJIS}). - A blank line.
- Six rows, in this fixed order (
ROW_KEYS): spent, tokens, price, marketCap, wallet, position.positionis skipped only when its value is empty (the pre-buy balance couldn't be read); any other row that renders empty is skipped the same way. - A blank line, then a tail of links on one line, joined with
|: Tx, Chart, the owner'sextraLinks(max 4), then whichever site-link slots are set.
Default row templates, each with its default icon
| Setting | Value |
|---|---|
| spent 💰 | {AMOUNT} {NATIVE} ({USD}) |
| tokens 🪙 | {TOKENS} {TOKEN} |
| price 🏷 | Price: {PRICE} |
| marketCap 🔼 | Mkt Cap: {MCAP} |
| wallet 🔍 | {WALLET} |
| position ⬆️ | Position: {POSITION} |
Default tokens row, rendered
| Input | Output |
|---|---|
| Template (tokens row, default icon) | 🪙 {TOKENS} {TOKEN} |
| Rendered | 🪙 1,000 NOT |
Bold markup omitted above; on Telegram “1,000 NOT” renders bold.
The line-drop rule
Applies only once the buy bot has a custom body set. The default layout above is never subject to it.
- The body is split into lines on the newlines inside its text runs (
_split_lines) — that is the unit the rule works on. - For each line, its placeholders are collected, excluding the four identity placeholders.
- A line is dropped when it has at least one non-identity placeholder, every one of them renders empty, and the rendered line contains no
<aanchor. - A blank line directly after a dropped line is swallowed too, so a drop never leaves a double gap.
Line-drop rule, rendered
| Input | Output |
|---|---|
| Template | Holders: {HOLDERS} Buy on {TOKEN}! |
| Rendered | Buy on NOT! |
{HOLDERS} has no value on this market, so the whole first line — including its literal Holders: text — is dropped. {TOKEN} is an identity placeholder, so the second line is never a drop candidate.
Note
Only 10 of the 20 placeholders can ever render empty in practice:
{POSITION},{TX},{CHART},{CA},{DEX},{HOLDS},{LIQ},{VOL24},{BUYS24},{HOLDERS}. The other six non-identity placeholders —{AMOUNT},{USD},{TOKENS},{PRICE},{MCAP},{WALLET}— are built from formatters that always return text, so a line built only from one of those six is never dropped.
Size-emoji scaling
Size row (buybot_helper.py)
n = max(1, min(maxEmojis, ceil(usd / usdPerEmoji)))n is how many times the size emoji (sizeEmoji, falling back to 🟢) repeats inside {EMOJIS}. usdPerEmoji == 0 forces n = 1.
| Setting | Value |
|---|---|
usdPerEmoji | USD cost of one size-row emoji. Clamped 0.01–1e9. |
maxEmojis | Caps the row length. Clamped 1–80, default 50. |
The dashboard's "Emojis per $10" field writes usdPerEmoji = 10 ÷ n for whatever number the owner enters there.
Caption fitting
Note
A 50-emoji size row costs 100 units to Telegram, not the length of the raw
tg-emojimarkup.
| Setting | Value |
|---|---|
| With media (photo, GIF or video attached) | 1024 units |
| Text-only | 4096 units |
Lines that don't fit are dropped whole from the end, never cut inside a tag. If even the first line doesn't fit on its own, it is truncated character by character instead, surrogate-pair aware.
Commands and the settings card
Every buy bot command, every button on the settings card, and the state the bot holds while it waits for an answer.
Forking a layout and paste-to-import
`/forkbuybot` in Telegram and pasting a buy post into the dashboard's body editor both run through the same import mapper (`lib/buybot-import.ts`) and…