iFragment

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 stepBefore (your own code)After (iFragment)
Check the usernamesearchStarsRecipient in Fragment with cookiesGET /v1/recipients/lookup
Buy after paymentinitBuyStarsRequest → getBuyStarsLink → sign and send TONPOST /v1/orders with externalId = your order id
Get the resultseqno polling, transaction matching, manual investigationsorder.succeeded / order.failed webhook
Session, wallet, throttling, retriesyoursours (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:

ifragment.ts
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:

on-paid.ts
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:

webhook.ts
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

  1. Get a Project key and a webhook secret; enable the IP allowlist for the bot's address.
  2. If you keep your own accounts, submit the session and wallet through the dashboard (OWN mode). From then on, use the wallet only through iFragment.
  3. Replace the recipient search with GET /v1/recipients/lookup.
  4. Replace the purchase with POST /v1/orders with externalId = the bot's order id.
  5. Add a webhook endpoint, signature verification and deduplication by event.id.
  6. Remove the Fragment/TON code: cookies, mnemonic, transfer signing, seqno polling.

On this page