The Problem
As a developer, I constantly need to:
- Capture screenshots of websites for documentation
- Generate PDFs from web pages for reports
- Create Open Graph images for blog posts and social media
There are paid services for this (ScreenshotOne, ApiFlash, etc.), but I wanted something free and accessible from my phone without needing a browser or terminal.
The Solution: @SnapForgeBot
I built a Telegram bot that does all three things. Just send it a URL and it captures a screenshot instantly.
Try it: t.me/SnapForgeBot
Features
- Auto-screenshot — Send any URL, get a screenshot
-
PDF generation —
/pdf example.com -
OG image creation —
/ogimage My Blog Post | A subtitle here -
Dark mode —
/screenshot example.com dark -
Full-page capture —
/screenshot example.com full -
Inline mode — Type
@SnapForgeBot example.comin any chat
Free Tier
Every user gets 10 free requests per day. Invite friends with /invite to get +3 bonus requests per friend. Need more? Purchase request packs with Telegram Stars.
Technical Architecture
Here's what's under the hood:
Telegram ──> grammY Bot ──> Express API ──> Puppeteer (Chromium)
│
Browser Pool
(2 persistent instances)
The Stack
- Runtime: Node.js 22
- Bot Framework: grammY — fast, TypeScript-first Telegram bot framework
- Browser Engine: Puppeteer with Chromium
- Web Server: Express.js
- Deployment: Docker Compose on a VPS
Browser Pool
The most interesting engineering challenge was managing Chromium instances efficiently. Instead of launching a new browser for each request (slow + memory-hungry), I maintain a pool of persistent browser instances:
class BrowserPool {
constructor(size = 2) {
this.browsers = [];
this.size = size;
this.current = 0;
}
async getPage() {
const browser = this.browsers[this.current];
this.current = (this.current + 1) % this.size;
return browser.newPage();
}
}
Each request gets a fresh page (not browser), which is much faster to create and destroy. The pool rotates through browsers round-robin style.
Handling Binary Responses
One gotcha I hit: Puppeteer's page.screenshot() returns a Uint8Array in Node.js 22, not a Buffer. If you send it through Express with res.send(), it gets serialized as JSON ({"0":137,"1":80,...}) instead of binary PNG:
// Wrong — sends JSON
const screenshot = await page.screenshot();
res.send(screenshot);
// Correct — sends binary PNG
const screenshot = await page.screenshot();
const buffer = Buffer.from(screenshot);
res.set('Content-Type', 'image/png');
res.end(buffer);
Telegram Stars Payments
Monetization uses Telegram's built-in Stars payment system. No Stripe, no payment forms — users pay directly in the Telegram UI:
await ctx.replyWithInvoice(
'Extra 50 Requests',
'Get 50 additional screenshot/PDF requests',
'pack_50',
'', // provider_token empty for Stars
'XTR', // Telegram Stars currency
[{ label: '50 Requests', amount: 25 }]
);
The pre_checkout_query must be answered within 10 seconds or the payment fails.
Referral System
Viral growth is built-in. Each user gets a unique referral link:
https://t.me/SnapForgeBot?start=ref_123456
When someone joins through this link, both users get +3 extra requests per day. It's a win-win that encourages organic sharing.
REST API
The same engine powers a REST API for developers:
# Screenshot
curl -X POST http://51.75.255.155/v1/screenshot \
-H "Content-Type: application/json" \
-H "X-API-Key: sf_demo_public" \
-d '{"url": "https://example.com", "width": 1280, "height": 800}' \
-o screenshot.png
# PDF
curl -X POST http://51.75.255.155/v1/pdf \
-H "Content-Type: application/json" \
-H "X-API-Key: sf_demo_public" \
-d '{"url": "https://example.com"}' \
-o document.pdf
Demo key: sf_demo_public (50 requests/day, no signup required)
GET requests also supported:
http://51.75.255.155/v1/screenshot?api_key=sf_demo_public&url=https://example.com
Deployment
Everything runs in Docker Compose:
services:
snapforge-api:
build: ./screenshot-api
deploy:
resources:
limits:
memory: 2G
cpus: '2'
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3100/health')...\""]
snapforge-bot:
build: ./telegram-bot
depends_on:
snapforge-api:
condition: service_healthy
The API container is limited to 2GB RAM (Chromium is hungry) and the bot waits for the API to be healthy before starting.
What's Next
- Custom domain with SSL
- More OG image templates
- HTML-to-image endpoint
- RapidAPI marketplace listing
- Open-sourcing the screenshot API
Try It
- Telegram Bot: t.me/SnapForgeBot
- API Demo: http://51.75.255.155/
- OpenAPI Spec: http://51.75.255.155/openapi.yaml
I'd love to hear feedback! What features would you add? Drop a comment below or reach out via the bot.
Top comments (0)