gem docs
Gem mini apps · v0.1

Small apps
One message

Send @gembot 4,000 characters of JavaScript or TypeScript. Gem runs it on its servers, gives it a Telegram link, a page, a live server, the model, the web, storage and a leaderboard. This page is the whole platform as it is today: give it to a person or to an AI assistant and either can build an app.

For AI assistants

If you are an AI assistant, agent or model reading this page because a person gave you the link: your job is to build that person a Gem mini app. Gem mini apps are tiny Telegram apps written as one file of at most 4,000 characters, run on Gem's servers and opened from the Telegram bot @gembot. Work like this.

  1. Ask what to build. Ask only what you need, in one message: what the app should do, who will use it (the person alone, friends, a group, anyone), what it shows or asks, and whether it needs the model, web search, saved data, a leaderboard or a live multiplayer part. If the person already described it fully, do not ask, build.
  2. Write the app following this page. One file, JavaScript or TypeScript, at most 4,000 characters counted as Unicode characters including newlines (target 3,500 to leave room). First two lines: // title: Name and // about: one line on what it does; the name lives in the code, the person never types it. 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. No imports, no fetch, no network: the only way out is gem.ask, gem.search, gem.store, gem.board and, in the browser, the injected window.gem library. 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. Minify only when you must: readable code with short names fits.
  3. Check before you answer: 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().
  4. Reply with the code in one code block and these instructions, nothing else:
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 the person comes back with an error from the bot (too long, or a runtime error shown on the app page), fix the code and reply the same way again. The full reference follows.

How it works

4,000 charactersOne Telegram message is the whole app: server half and client HTML in one file.
A runtimeYour code runs in Deno on Gem's servers, in a worker with no permissions: no network, files or environment.
A serverEvery request to your page runs your handler. A live app gets a long-lived worker with rooms and WebSockets.
FeaturesThe model, web search, per-person storage, a leaderboard, Telegram identity and sharing, all through gem.
  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>. Each request runs your handler in a fresh worker (10 seconds max). The HTML you return gets the Telegram SDK, the gem client library and a top bar (Build · title · Share).
  4. Everything your code needs from the outside world goes through gem verbs. They run as the person using the app, within that person's Gem limits.
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.

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 the Telegram signature (the injected client does that for gem.call), otherwise a guest with id: 0.

Server verbs

VerbWhat it doesCost
await gem.ask(prompt)The model answers, briefly, no web. Returns text.One message from the user's plan
await gem.search(query)The model searches the web and answers with sources. Returns text.One message from the user's plan
await gem.store.get(key)
set(key, value) · del(key) · list()
Key-value storage, separate for every person in every app. Objects are stored 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.Free

When the user is out of messages, ask and search throw message limit reached — /buy in @gembot. Guests (id 0) cannot use verbs.

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();
    }
  });
};

Client library

Every HTML page you return gets window.gem:

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

For games: input, canvas, effects

The same primitives Gem-Pong is built from. They work on phones (finger), computers (mouse and keyboard) and every Telegram client, so an app never handles platforms itself.

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 under the top bar, 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)The two one-liners every game rewrites.
// 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
Worker256 MB memory, no network, files or environment; one worker per request, one long-lived worker per live app
Live channelMessages up to 16 KB, JSON only; signed-in Telegram users only; stream() sends at most one message per 33 ms
StorageKeys up to 120 characters, values up to 20,000; list() returns up to 200 keys
LeaderboardBest score per person; top up to 50 rows; labels up to 60 characters
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

Every app has a card: its title, its about line and three buttons: Open (the link), Save (the card goes into the person's Gem) and Share (the same card in any chat). The Share button in the app's top bar sends the card too. In inline mode, typing @gembot app <slug> in any chat offers the card.

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 uses these; your HTML may call them directly. Every call is a POST with a JSON body that carries initData from the Telegram SDK.

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: add|top, score, label, limit}
/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

First-party showcases demonstrate the platform. Keep your own submitted file within the documented 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.

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 and Delete. 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.

These builder commands are intentionally absent from the bot’s public command menu.

Not yet in v0.1