Telegram bots are how a large share of Solana traders actually trade. The interface is trivial. What makes one usable is everything behind the message handler — and that is where most builds go wrong.
The defining constraint: a user pressing Buy expects a result in seconds, and they are not watching logs when it fails.
Separate the Chat Layer From the Trade Engine
The first architectural mistake is putting trading logic inside message handlers.
// ★Do not do this.★
bot.on("callback_query", async (q) => {
const quote = await getQuote(...);
const tx = await buildSwap(quote);
const sig = await connection.sendRawTransaction(tx.serialize());
await bot.answerCallbackQuery(q.id, { text: `Sent: ${sig}` });
});
Three problems, all of which surface only under load:
Telegram expects a fast acknowledgement. Holding the handler open through quote, build, sign, and send means slow responses and timeouts.
A restart loses in-flight trades. State lives in the handler's closure, so a deploy mid-trade leaves the user with no result and you with no record.
Concurrency is unbounded. Fifty users pressing Buy on the same launch means fifty concurrent handlers.
★The shape that works is a queue between the two.★
bot.on("callback_query", async (q) => {
const jobId = await queue.push({
userId: q.from.id, action: "buy", mint, amount,
});
await bot.answerCallbackQuery(q.id, { text: "Submitting…" });
// ★Handler returns immediately. Worker owns the trade.★
});
The worker sends, retries, resolves to a terminal state, and pushes the result back to the user as a message edit. The chat layer becomes a view, and the trade engine becomes something you can test, restart, and scale independently.
Key Handling Is the Product
Every Telegram trading bot answers one question, and users judge it on the answer: who holds the private key?
Bot-held keys. The bot generates a wallet and stores the key. Simple, fast, and it means users are trusting your infrastructure with funds. This is the model most bots use and the reason most bot incidents are catastrophic rather than annoying.
User-signed. The bot builds a transaction and the user signs elsewhere. Safe, and a poor fit for one-tap trading — the round trip defeats the point.
Delegated with on-chain limits. The user authorises specific operations up to specific bounds. More work to build, and the failure mode is bounded rather than total.
★If you hold keys, the security requirements are not optional extras:★
- Encryption at rest with keys held outside the application database
- No private key ever written to a log line, an error message, or an exception trace
- Withdrawal limits and confirmation steps that survive a compromised session
- ★Assume your Telegram bot token will leak at some point and design so that it alone is not enough★
Be explicit in your interface about which model you use. Users who have been through a bot incident ask this first, and a vague answer is itself an answer.
If your bot is built and users are complaining about missed fills, a free BoltTx key is one line to test the submission path.
The Concurrency Spike Is the Real Load Test
Telegram bot traffic is not smooth. It is flat, then a token launches and every user acts within the same few seconds.
// ★Per-user serialisation, global concurrency bound.★
const userLocks = new Map();
async function runTrade(userId, job) {
const prev = userLocks.get(userId) ?? Promise.resolve();
const next = prev.then(() => globalLimiter.run(() => execute(job)));
userLocks.set(userId, next.catch(() => {}));
return next;
}
★Two different limits, and both are necessary.★ Per-user serialisation stops one user's double-tap from sending two buys. The global bound stops a launch spike from exhausting your RPC rate limit — at which point every user fails simultaneously, including the ones who would otherwise have been fine.
Report Terminal States, Not Submissions
The most common complaint about trading bots is "it said it worked and it did not." That is almost always this bug:
const sig = await connection.sendRawTransaction(raw);
await bot.editMessageText("✅ Bought!"); // ★Wrong. Nothing landed yet.★
A signature means the RPC accepted the bytes. Users need the outcome, and there are three.
await bot.editMessageText("⏳ Submitted…");
const outcome = await resolveTransaction(sig, lastValidBlockHeight);
switch (outcome.status) {
case "success":
await bot.editMessageText(`✅ Bought ${amt} — ${short(sig)}`);
break;
case "reverted":
// ★Landed and failed. Explain why in their terms.★
await bot.editMessageText(`❌ Failed: ${explain(outcome.err)}`);
break;
case "expired":
// ★Never landed. Nothing was spent beyond the attempt.★
await bot.editMessageText("⚠️ Did not land. Retry?");
break;
}
★The distinction between reverted and expired is the one users most need and most rarely get.★ A revert means their trade executed and hit a condition — slippage, balance, an account that did not exist. An expiry means it never ran at all. Telling a user "failed" for both leaves them unable to decide whether to retry or change something.
Translate errors into their language:
| Chain error | What to say |
|---|---|
| slippage exceeded | Price moved — raise slippage or reduce size |
| insufficient funds | Not enough SOL for amount plus fees |
| account not found | Token account needs creating first |
| blockhash not found | Did not land in time — safe to retry |
Fees Are Your Margin
If you charge a percentage and pay priority fees from your own pocket, ★your fee policy is your margin policy.★
Do not hardcode. A fixed priority fee overpays in calm conditions and underpays during launches, which are the trades users judge you on.
Scale to trade size. A large buy justifies more fee than a small one. Charging the same for both means your economics are wrong at one end.
Cap it and surface the cap. During a fee spike, an unbounded policy is an unbounded loss. Tell the user that conditions are extreme rather than silently paying or silently failing.
Decide who pays and say so. Users notice when the effective cost differs from the advertised percentage, and finding out from a block explorer damages trust more than the fee itself.
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★For a user-facing bot this is a product metric, not an infrastructure one.★ Users compare bots on whether their buy went through during the launch, and they switch on the basis of a handful of bad experiences.
Where BoltTx Fits
We handle submission. Not the chat layer, not custody, not your fee model.
Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so user trades are not observable in transit before they land — which matters when your users are all buying the same launch at the same moment.
Your bot signs locally with whatever key model you chose. We never hold funds, never sign, and never modify transaction contents. The tip travels inside the transaction, paid on chain from the signing wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.
Get a free API key. No monthly fee:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
How do I build a Telegram trading bot for Solana? Separate the chat layer from the trade engine with a queue between them. Message handlers acknowledge immediately, and a worker owns quoting, sending, retrying, and reporting the terminal state.
Should a Telegram bot hold user private keys? It is the model most bots use because it enables one-tap trading, and it means users trust your infrastructure with funds. If you choose it, state so plainly in your interface and treat encryption, log hygiene, and withdrawal limits as requirements.
Why does my bot say a trade succeeded when it did not?
Because it reports the signature from sendRawTransaction rather than the outcome. A signature means the RPC accepted the bytes, not that anything landed. Resolve to success, reverted, or expired before telling the user.
How do I handle many users trading at once? Serialise per user so a double-tap does not send two buys, and bound global concurrency so a launch spike does not exhaust your rate limit and fail every user simultaneously.
What should I tell users when a trade fails? Distinguish reverted from expired. A revert executed and hit a condition like slippage, so something must change. An expiry never ran, so retrying as-is is reasonable.
What slippage should a Telegram bot default to? Higher for new launches than for established pairs, and it should be user-adjustable. A single global default is either too tight for launches or too loose for liquid pairs.
How do I stop double-buys from repeated taps? Per-user serialisation plus deduplication on the callback query id. Disabling the button in the UI is not sufficient, since the callback can already be in flight.
Should the bot or the user pay priority fees? Either works, but say which in your interface. Users notice when effective cost differs from the advertised percentage, and discovering it from a block explorer damages trust more than the fee would have.
How do I keep a Telegram bot responsive during launches? Acknowledge callbacks immediately and do the work in a queue. Holding the handler open through quote, build, and send causes timeouts precisely when volume is highest.
What happens to in-flight trades when the bot restarts? If state lives in the handler, they are lost with no result for the user and no record for you. Persist jobs and their signatures so a worker can resume resolving them after a restart.
How do I show transaction status to users? Edit the original message through the stages rather than sending new ones. Submitted, then the terminal state, keeps the chat clean and gives the user one place to look.
Should each user get their own wallet? Generally yes if you hold keys, since a shared wallet makes accounting hard and turns one compromise into every user's problem. Per-user wallets also make on-chain history legible to each user.
How do I estimate SOL needed for a trade? Trade amount plus base fee, plus priority fee, plus rent if a token account must be created. Bots that check only the trade amount produce insufficient-funds reverts users find confusing.
Why do my users lose launch races? Usually the submission path rather than the bot logic. Measure slot distance from send to landing during a launch, since queue depth and RPC rate limits both degrade at exactly that moment.
Is skipPreflight safe for a user-facing bot? Yes, and it is generally correct — preflight adds a round trip and simulates the wrong slot. Simulate during development, and validate balance and accounts before building instead.
How do I test a Telegram bot under launch conditions? Load-test the queue and rate limits with concurrent synthetic users, not one user at a time. A bot that works for a single trader tells you nothing about how it behaves when fifty buy at once.