gem docs
Gem mini apps · v0.1

Send code Get a working link

Write the handler and page in one JavaScript or TypeScript file. Send it to @gembot, and Gem replies with a link to your app in Telegram. Use Gem’s built-in tools for storage, AI, search and sharing.

Make a tool or a game

Try the counter below, or give your coding assistant the build instructions. Your file can contain up to 4,000 characters.

Turn a document into an app

Already have a guide, menu or manual? Send the document to get sections, search and answers from its contents. No code needed.

Run the counter, then make it yours

  1. Open Gem’s app builder and send the complete code below as one message.
  2. Tap Open in Gem’s reply. Tap the counter a few times, close the app and open it again: your number is saved.
  3. Use Share to send the app to a friend. They get their own counter, starting at zero.
// title: Counter
// about: Counts your taps, for you only
export default async (req, gem) => {
  if (req.method === 'POST') {
    const n = (Number(await gem.store.get('n')) || 0) + 1;
    await gem.store.set('n', n);
    return {n};
  }
  return `<button onclick="gem.call('/', {}).then(r => this.textContent = r.n)">
    ${(await gem.store.get('n')) || 0}</button>`;
};

Try an update. Change the increment from + 1 to + 2. In @gembot, send /myapps, choose Edit for your counter and send the complete replacement file. Open the same link to try the change.

For your own idea, give a coding assistant the build instructions. Use the interface guide for screens and controls, and the style guide for Gem’s type, colors and icons.

Browse the reference

For AI assistants

Give this page to your coding assistant together with the job you want the app to do. For example: “Make a counter I can use at the gym. Save my count, let me reset it, and use Telegram’s colors.” The assistant should return one complete file and the steps to launch it.

  1. Define the first useful version. Work from the person’s description. Establish who uses the app, what they do on its first screen and what needs to be saved. Add AI, search, scores or live interaction only when the job needs them. Ask for missing requirements together; if the brief is complete, build.
  2. Write one complete file. Use JavaScript or TypeScript and stay within 4,000 Unicode characters, including newlines. Aim for 3,500 to leave room for fixes. First two lines: // title: Name and // about: one line on what it does; Gem reads the app’s name from the file. Export the handler export default (req, gem) => … that returns HTML (a string), a Response or an object; add export const live = gem => … only when the app needs rooms and real-time messages. Keep the code readable; shorten it only as needed to meet the size limit.
  3. Use Gem for services and data. No imports, no fetch, no network: the only way out is gem.ask, gem.search, gem.store, gem.board, gem.kb and, in the browser, the injected window.gem library. Leaderboard scores are written by server code only (gem.board(name).add in export default or live); the page reads them with gem.top or gem.best.
  4. Use the document route when it fits. If the app is built around a document (a guide, a manual, a menu, a course), tell the person they can send the document itself to @gembot after /newapp; for a design of your own, use gem.guide({...}) or gem.kb (see Knowledge apps).
  5. Connect the page after it loads. Put page logic in the returned HTML inside onload = () => { … } because the library loads after your script. Use gem.theme colors so the app fits light and dark Telegram themes.
  6. Check before sending: character count under 4,000; the two header lines present; export default present; no import, require or fetch in the server half; every gem.ask or gem.search call is worth a message from the person's plan; guests (gem.user.id === 0) handled or sent to Telegram with gem.guest(); leaderboard scores added only in server code; no telegram-web-app.js script tag (Gem provides Telegram.WebApp).
  7. Deliver the whole file. Put it in one code block, ready to paste. Include these launch steps:
Your app is ready. To launch it:
1. Open https://t.me/gembot?start=newapp
2. Paste the whole code below as one message.
3. The bot replies with your app's link. Open it, and share it from the Share button inside the app.

```js
// title: …
// about: …
export default …
```

If Gem rejects the file or the app fails, fix the reported error and return the complete replacement file. Use Edit to update the existing app; a fresh /newapp request creates another one.

How it works

One file to send

The handler and the HTML it returns share the 4,000-character budget. You send both in the same message.

Two places to run

The handler runs in a Deno worker on Gem. Its returned page runs in a separate browser sandbox. Server code has no network, filesystem or environment access.

Requests or live rooms

A page request runs the handler once. For a game or shared session, a live worker stays running to manage rooms and WebSocket messages.

Built-in services

Use gem for AI, web search, personal storage, leaderboards, identity and sharing.

  1. Open https://t.me/gembot?start=newapp (or send /newapp to @gembot). The next message you send is the code; the app's name is the // title: line inside it.
  2. Gem stores it under a six-character slug and answers with the link https://t.me/gembot?startapp=<slug>.
  3. Opening the link opens the Mini App at https://apps.gem.bot/<slug>: Gem's frame with the top bar (Build · title · Share), and your page inside it. Each request runs your handler in a fresh worker (10 seconds max). The HTML you return gets the gem client library and Gem's own Telegram.WebApp.
  4. Use the gem APIs for external capabilities. Requests run on behalf of the person using the app and use that person’s allowance.
Start the file with // title: Name and // about: what it does. They name and describe the app on its card and in the top bar.

Your page is sandboxed

The handler

// title: Hello
// about: Greets whoever opens it
export default async (req, gem) => `<h1>Hi, ${gem.user.name || 'stranger'}</h1>`

req is a standard Request for https://apps.gem.bot/<slug>/<path> with method, query and body. Return an HTML string, a Response, or any object (sent as JSON).

gem.user is {id, name, language, premium}. It is the signed-in Telegram person when the request carries your app's pass (the client library does that for gem.call), otherwise a guest with id: 0. The pass itself never reaches your code.

Server verbs

VerbWhat it doesCost
await gem.ask(prompt)Returns a short AI answer as text. Does not search the web.One message from the user's plan
await gem.search(query)Searches the web and returns an AI answer with sources as text.One message from the user's plan
await gem.store.get(key)
set(key, value) · del(key) · list()
Read and write key-value data for the current person in this app. Objects are saved as JSON text.Free
await gem.board(name).add(score, label)
await gem.board(name).top(limit)
Leaderboards. Keeps the best score per person; top returns [{person, name, score}] with Telegram names. Scores are added only here, in server code: pages read the board but cannot write to it.Free
await gem.kb.list(opts)
gem.kb.search(query, opts) · gem.kb.ask(question, opts)
Your app's knowledge (see Knowledge apps): the whole document in order, search by words, typos and meaning, and answers only from the document with numbered sources.ask: one message; the rest free

When the person has used their message allowance, ask and search throw message limit reached — /buy in @gembot. Guests (id 0) can browse knowledge and search it by words. AI, personal storage, leaderboards and live connections require Telegram sign-in.

Live apps

Export live and your app also gets a long-lived worker. People connect to it over WebSocket, the library puts them into rooms, and your code reacts to events. This is how Gem-Pong pairs two players and moves the ball.

export const live = gem => {
  gem.rooms(2, {
    start(room) {                 // `size` players joined: room.players = [{id, name, index}]
      room.send({go: room.players.map(p => p.name)});
      room.t = setInterval(() => room.send({tick: Date.now()}), 33);
    },
    message(room, player, msg) {  // any JSON a player sends while in the room
      room.to(player, {echo: msg});
    },
    leave(room, player) {         // a player disconnected
      clearInterval(room.t); room.end();
    }
  });
};

Knowledge apps

Turn a document into a browsable, searchable app. People can read the cards or ask questions and follow numbered citations back to the relevant source. This works for guides, manuals, menus, courses and price lists. For example: Mausfit, a nutrition guide made from one PDF.

// title: Our menu
// about: The menu, searchable, with answers
export default () => `<script>onload = () => gem.guide({
  title: 'Our menu', accent: '#8E1B2C',
  questions: ['What is vegan?', 'What is not spicy?'],
  style: 'Answer like a friendly waiter, in two sentences.'
})</script>`;
// Then: /myapps → Knowledge → send the menu as a PDF or text.

For richer cards, send JSON: [{section, title, body, url, tags, meta}]. meta.kind picks the card: product (with n, brand, shop, more), tip, recipe, place, combo (with meal), lead (a section's intro line) or cta; url becomes the card's button.

Client library

Gem injects window.gem into the HTML your handler returns. Use these helpers for requests, themes and interaction:

MemberPurpose
gem.user{id, name, language, premium} or null outside Telegram.
gem.call(path, data)POST to your own app as the signed-in person; the handler sees gem.user. The path is inside your app: '/save' goes to apps.gem.bot/{slug}/save (so req.url ends with /save). Returns parsed JSON or text.
gem.event(name)Report an event to the app’s project rules, for example await gem.event('completed'). Returns {ok: true}. A matching “app” rule fires once per person. Requires a signed app pass; standalone apps return 404. Call after the action succeeds, not when a page merely opens.
gem.app.projectFor an attached app: {name, title, langs, url}, also available as window.GEM_APP.project. Absent for standalone apps. Use it to show project context; it is not proof of authorization.
gem.live({open, wait, message, error, close})WebSocket channel to your live worker: send(obj) right away, stream(obj) for “my latest state” (at most one message per 33 ms, only when it changed), join(opts), close(). Returns null outside Telegram and shows the guest screen.
gem.theme · gem.safe · gem.darkTelegram colors (bg, bg2, text, hint, link, accent, button, buttonText), safe insets (top, bottom) so nothing hides under the phone's bars, and whether the theme is dark.
gem.color(hex, alpha) · gem.mix(a, b, t)Theme colors with transparency or blended: for glows, trails and panels.
gem.pctrue on a computer (Telegram Desktop, macOS, Telegram Web, or a mouse with hover). For hints only: input is always listened to in full.
gem.sound.beep(freq, ms, type, vol) · gem.music.start/stop/toggle · gem.music.button()Synthesised sounds, a looping tune and a ready “♪ Music on/off” button. Start sound from a tap: phones require it.
gem.haptic(kind)light, medium, heavy, success, error, warning, select.
gem.screen(html, {label: fn}) · gem.hide()A full-screen overlay in theme colors with buttons; the first one is the main button (Enter and Space press it), a handler that returns a string relabels its button. <b> is a title, <small> a hint.
gem.guest()Outside Telegram: shows “Open in Telegram” and returns true.
gem.top(name, n) · gem.best(name, n) · gem.esc(text)Leaderboard rows as data or as ready HTML; HTML escaping for names and any text from people. Scores are added in server code.
gem.kb.list/search/ask · gem.guide({...})Your app's knowledge and the ready-made guide (see Knowledge apps).

For games: input, canvas, effects

These helpers power Gem-Pong and support touch, mouse and keyboard input. Use them to keep the same interaction working across Telegram clients without separate controls for each device.

MemberPurpose
gem.cursor(el, {x, y, speed, min, max, axis, hover})A point in 0..1 inside an element that is always current: the finger drags it, the mouse moves it even without pressing, arrow keys and WASD move it at speed widths per second. Read cursor.x / cursor.y every frame: this is your paddle, bar or crosshair with no server round trip.
gem.keysHeld keys by e.code (layout independent), axes x and y from arrows or WASD, press(code, e) hook. The library takes keyboard focus inside Telegram Desktop and Telegram Web, stops arrows from scrolling, and releases keys when the window loses focus.
gem.loop(fn(dt, t))One shared requestAnimationFrame for the page; dt in seconds, never more than 0.05, stands still while the app is hidden. Returns {stop, start}.
gem.canvas()Full-screen canvas (Gem's top bar is outside your page), scaled for the device pixel ratio, no text selection or context menu, vertical swipes do not close the app. Fields: el, ctx, w, h, onresize. Drawing helpers chain: rect(x, y, w, h, radius, color, glow, stroke), dot(x, y, r, color, glow), line(x0, y0, x1, y1, color, width, dash), text(str, x, y, size, color, weight, align), heart, hearts(x, y, n, total, color), grad, clear(), flash(color, ms, alpha), shake(px, ms), and loop(fn) that clears, shakes and flashes around your frame.
gem.tween(ms, keys)Smooths server ticks for the screen: push(state) on every message, get() every frame moves numbers from what is drawn to the newest state over one tick; cut() before a teleport. keys names the fields to smooth.
gem.trail(n) · gem.sparks()A comet tail (push(x, y), draw(c, r, color), clear()) and particle bursts (add(x, y, color, n, speed), draw(c, dt)).
gem.touch(el, (x, y, phase) => …)Raw pointer events in element pixels; phase is down, move, up, or hover for a mouse with no button down.
gem.clamp(v, lo, hi) · gem.lerp(a, b, t)Clamp a number to a range or interpolate between two values.
// A paddle you control, drawn every frame, sent to the server when it changes
const c = gem.canvas(), cur = gem.cursor(c.el, {min: .1, max: .9, axis: 'x'});
const ch = gem.live({message: d => tw.push(d)}), tw = gem.tween(33, ['ball']);
c.loop(dt => {
  ch.stream({x: cur.x});
  const s = tw.get(); if (!s) return;
  c.rect(cur.x * c.w - 40, c.h - 120, 80, 12, 6, gem.theme.button, 14)
   .dot(s.ball[0] * c.w, s.ball[1] * c.h, 8, gem.theme.accent, 16);
});

Limits

WhatLimit
Code4,000 characters, one message, JavaScript or TypeScript
Request10 seconds per handler run, then 504; response up to 512 KB; request body up to 256 KB
WorkerNo network, files or environment; one worker per request, one long-lived worker per live app. Apps share runner processes with a 192 MB memory ceiling each: an app that runs out of memory is stopped (503) and its process restarts in about a second
Live channelMessages up to 16 KB, JSON only, up to 40 a second per connection; signed-in Telegram users only; stream() sends at most one message per 33 ms; the worker stops after 2 minutes with nobody connected, or after 15 seconds without a response
StorageKeys up to 120 characters, values up to 20,000 characters (longer is an error, not a cut), up to 1,000 keys per person per app; list() returns up to 200 keys
LeaderboardBest score per person; scores from server code only; top up to 50 rows; labels up to 60 characters
Calls per minutePer person per app: storage 120, leaderboard 60, share 10, knowledge search 30 (guests: 300 for the whole app). Over the limit: 429 too many requests
KnowledgeUp to 1,500 entries from a document sent in chat; answers use the 10 closest entries; each ask is one message
Model and webPrompts up to 4,000 characters; each call is one message from the plan of the person using the app
SlugSix random lowercase letters and digits for a new app; first-party slugs may be 4–8 characters

Cards and sharing

Gem gives each app a card with its title and description. Open launches it, Save keeps it in the person’s Gem, and Share sends the card to another chat. The app’s top-bar Share button uses the same card. You can also find it from any chat by typing @gembot app <slug>.

Your app's dashboard

Every app has a dashboard for its owner: https://apps.gem.bot/{slug}/db, or from Telegram with the deep link https://t.me/gembot?startapp={slug}-db. It shows people (all time, last 7 days, today), opens, live sessions, actions (model asks, web searches, app calls, saves, scores, shares), storage, every leaderboard's top five and a 14-day chart. Only the person who sent the code can open it.

HTTP endpoints

The client library wraps these HTTP endpoints. You can also call them from your app’s HTML. Use POST with JSON; authenticated calls carry initData, the app-scoped pass exposed as Telegram.WebApp.initData. The table marks endpoints that allow guest access.

EndpointBody
/api/apps/{slug}/ask{initData, prompt} → {text}
/api/apps/{slug}/search{initData, query} → {text}
/api/apps/{slug}/store{initData, op: get|set|del|list, key, value}
/api/apps/{slug}/board{initData, name, op: top, limit} (adding scores is server code only)
/api/apps/{slug}/kb/list{doc?, section?, offset?, limit?} → {items, total, sections}, no sign-in needed
/api/apps/{slug}/kb/search{initData?, query, limit?, section?} → {hits}; by meaning only with initData
/api/apps/{slug}/kb/ask{initData, question, history?, style?, strict?} → {text, sources, cited}
/api/apps/{slug}/kb/load{initData, items | text, doc?} → {count}, the app's owner only
/api/apps/{slug}/share{initData} → prepared message id for Telegram.WebApp.shareMessage
wss://apps.gem.bot/{slug}/wsfirst frame {init: initData}, then JSON both ways

Examples

A counter with storage

// title: Counter
// about: Counts your taps, for you only
export default async (req, gem) => {
  if (req.method === 'POST') {
    const n = (Number(await gem.store.get('n')) || 0) + 1;
    await gem.store.set('n', n);
    return {n};
  }
  return `<button onclick="gem.call('/', {}).then(r => this.textContent = r.n)">
    ${(await gem.store.get('n')) || 0}</button>`;
};

Ask the model

// title: Haiku
// about: A haiku about anything
export default async (req, gem) => {
  const topic = new URL(req.url).searchParams.get('q') || 'rain';
  return `<pre>${await gem.ask('Write a haiku about ' + topic)}</pre>
  <form><input name=q placeholder="topic"></form>`;
};

Showcase

Open these apps to see the APIs in use. They are maintained by Gem; your submitted code must still fit the 4,000-character limit.

Интуиция is a first-party multiplayer quiz: the model writes a question with ten options, the question reveals letter by letter, five stars each, first correct answer wins the round, a lobby with a 15-second countdown, matches of two to four with spectators, a leaderboard by best match.

Pocket Gem shows every verb on one screen in 3,985 characters: your name and visit count (gem.store), a question to the model (gem.ask), a web search (gem.search), private notes (store.list/set/del) and a five-second tap game with a leaderboard (gem.board), plus haptics and sounds from the client library.

Mausfit is a knowledge app made from one PDF: a nutrition guide with 14 sections and 134 cards with shop links, search, answers with sources, a shopping list, a calculator and a quiz scored on the server, built on gem.guide with two tabs of its own.

Gem-Pong is a full live game in 3,998 characters: play against a random person from the public queue or against the computer (a server-side paddle), ball physics on the server at 30 ticks per second, rounds that get faster, three misses and you are out, a round counts only if you moved, three idle rounds and you are out of the match, leaderboards by best round (rounds and solo), finger, mouse or arrow keys, glowing paddles and ball with a trail, sparks, flash and shake, sounds, music and haptics.

Manage your apps

Send /myapps to the bot to see your apps. Each has Open, Edit, Admin, Delete and Knowledge. Edit accepts replacement code as the next message and keeps the same link. Admin opens the owner dashboard. Delete asks for confirmation, then removes the app and its data. Knowledge takes a file or pasted text and makes it the app's knowledge.

Use these commands directly in @gembot; they are not listed in the general command menu.

Not yet in v0.1