Integration example: a Telegram shop bot
How a bot like StarPay replaces its own Fragment/TON code with iFragment API calls.
A typical Stars shop bot (like StarPay) talks to Fragment on its own: it keeps session cookies and the TON wallet mnemonic, signs transfers, and tracks seqno and retries. With iFragment all of that is replaced by three API calls and one webhook. Customer payments, the catalog and the bot interface stay yours.
Before and after
| Bot step | Before (your own code) | After (iFragment) |
|---|---|---|
| Check the username | searchStarsRecipient in Fragment with cookies | GET /v1/recipients/lookup |
| Buy after payment | initBuyStarsRequest → getBuyStarsLink → sign and send TON | POST /v1/orders with externalId = your order id |
| Get the result | seqno polling, transaction matching, manual investigations | order.succeeded / order.failed webhook |
| Session, wallet, throttling, retries | yours | ours (Supplier Accounts) |
If you already have your own session and wallet, connect them as a Supplier Account in OWN mode: you keep paying the supplier yourself, and the platform takes over fulfilment.
Flow
Buyer ─▶ bot: /buy 500 → @durov
bot ─▶ iFragment: GET /v1/recipients/lookup → name and avatar, "Found"
Buyer ─▶ your payment (Payme, Click, Stars, …) → paid
bot ─▶ iFragment: POST /v1/orders { externalId: "<bot order id>" } → 201 QUEUED
iFragment ─▶ bot: webhook order.succeeded → "Done! 500 ⭐ sent to @durov"Code (TypeScript, fetch)
The iFragment client is a few lines on top of fetch:
const API = 'https://api.ifragment.com/v1';
async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${API}${path}`, {
...init,
headers: {
'X-API-Key': process.env.IFRAGMENT_KEY!,
'Content-Type': 'application/json',
'Accept-Language': 'en',
...init.headers,
},
});
const body = await res.json();
if (!body.success) {
throw Object.assign(new Error(body.error.message), { status: res.status, code: body.error.code });
}
return body.data as T;
}
export const ifragment = {
lookup: (username: string) =>
call<{ found: boolean; name?: string }>(`/recipients/lookup?username=${encodeURIComponent(username)}&product=STARS`),
// externalId = the bot's order id: a retry after a timeout will not create a second order.
createStarsOrder: (shopOrderId: string, username: string, quantity: number) =>
call<{ id: string; status: string }>('/orders', {
method: 'POST',
headers: { 'Idempotency-Key': `shop-order-${shopOrderId}` },
body: JSON.stringify({ product: 'STARS', quantity, username, externalId: shopOrderId }),
}),
};The purchase in the bot after a successful payment, replacing the old Fragment/TON code:
import { ifragment } from './ifragment';
export async function onShopOrderPaid(order: { id: string; username: string; quantity: number; chatId: number }) {
try {
await ifragment.createStarsOrder(order.id, order.username, order.quantity);
await db.shopOrders.update(order.id, { status: 'FULFILLING' });
await bot.api.sendMessage(order.chatId, 'Payment received, sending your stars…');
} catch (error) {
// 422: the recipient disappeared between the check and the payment, refund the buyer.
// 5xx/network: retry later with the same call: externalId and Idempotency-Key prevent duplicates.
await retryQueue.add('create-order', order, { delay: 30_000 });
}
}The webhook: verify the signature and notify the buyer. data.externalId is your order id, which is how we find the buyer:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(raw: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
return false;
}
const expected = createHmac('sha256', secret).update(`${parts.t}.${raw}`).digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && timingSafeEqual(expected, received);
}
export async function handleWebhook(request: Request): Promise<Response> {
const raw = await request.text(); // raw body, before JSON.parse
if (!verify(raw, request.headers.get('X-IFragment-Signature') ?? '', process.env.IFRAGMENT_WEBHOOK_SECRET!)) {
return new Response('bad signature', { status: 401 });
}
const event = JSON.parse(raw) as { id: string; type: string; data: { externalId: string; quantity: number } };
if (await db.processedEvents.exists(event.id)) {
return new Response('ok'); // redelivery
}
const order = await db.shopOrders.find(event.data.externalId);
if (event.type === 'order.succeeded') {
await db.shopOrders.update(order.id, { status: 'COMPLETED' });
await bot.api.sendMessage(order.chatId, `Done! ${event.data.quantity} ⭐ sent to @${order.username}`);
} else if (event.type === 'order.failed') {
await db.shopOrders.update(order.id, { status: 'FAILED' });
await bot.api.sendMessage(order.chatId, 'We could not fulfil your order and will refund you within a day.');
}
await db.processedEvents.add(event.id);
return new Response('ok');
}Migration checklist
- Get a Project key and a webhook secret; enable the IP allowlist for the bot's address.
- If you keep your own accounts, submit the session and wallet through the dashboard (
OWNmode). From then on, use the wallet only through iFragment. - Replace the recipient search with
GET /v1/recipients/lookup. - Replace the purchase with
POST /v1/orderswithexternalId= the bot's order id. - Add a webhook endpoint, signature verification and deduplication by
event.id. - Remove the Fragment/TON code: cookies, mnemonic, transfer signing, seqno polling.