Text your own model
Same bot that pings you when a job finishes — now text it a question and any model on OpenRouter answers. Your key, your box, no app, no middleman.
A real thread — your text in, the model's reply back, over Telegram.
notify and gate are godozo talking to you. listen flips it around: it long-polls for incoming texts, hands each to a command, and sends whatever that command prints back as the reply. Point it at OpenRouter and — that's it — you've got a private, self-hosted LLM you can text from anywhere, on any model, with nothing running but an outbound connection.
The whole thing: one script, one command
The script takes a message and prints an answer; godozo handles the phone side. Here's the whole bridge — zero dependencies, just Node's built-in fetch:
// ask.mjs — one text in, one model reply out. // The message arrives as $GODOZO_MESSAGE (never spliced into a shell string, // so it can't inject). Whatever we print to stdout gets texted back. const message = process.env.GODOZO_MESSAGE || ''; const KEY = process.env.OPENROUTER_API_KEY; const MODEL = process.env.MODEL || 'openai/gpt-4o-mini'; const SYSTEM = process.env.SYSTEM || 'You are a concise assistant answering over text. Keep replies short.'; try { const res = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: MODEL, messages: [{ role: 'system', content: SYSTEM }, { role: 'user', content: message }], }), }); const json = await res.json(); if (!res.ok) throw new Error(json.error?.message || `HTTP ${res.status}`); process.stdout.write(json.choices?.[0]?.message?.content?.trim() || '(no reply)'); } catch (e) { process.stdout.write(`⚠️ ${e.message}`); // errors come back as the reply, not a crash }
Then hand it to godozo:
# prove the pipe first — echo mode just repeats your text back godozo listen --echo # then the real thing: every text runs ask.mjs and texts the answer back export OPENROUTER_API_KEY=sk-or-... godozo listen --exec 'node ask.mjs'
And that's the whole thing. Text your bot "what's the difference between a mutex and a semaphore?" and the answer lands in the thread. Swap MODEL for any slug on openrouter.ai/models — a frontier model when you want depth, a cheap one for quick lookups — no app in sight.
Chat, not agent — and why that's the safe default
This bridge can only answer. ask.mjs calls one API and prints text; it has no shell, no tools, no access to your machine. Whatever gets texted in becomes the content of a chat message — never a command. That's the safe shape.
The dangerous shape is an agent bridge, where the exec command can act on what's texted:
# POWERFUL, RISKY — the agent can run tools driven by whatever you text, # unattended. Texting becomes acting. Only if you fully trust the channel. godozo listen --exec 'claude -p "$GODOZO_MESSAGE"'
Same plumbing, very different blast radius. For a text-a-model bridge, stay on the chat shape. Two more guardrails godozo gives you for free:
- Allowlist. Only ids in
GODOZO_TELEGRAM_ALLOW(default: your own chat id) can drive it. Anyone else texting the bot gets "Not authorized" — even though the bot is reachable by anyone. - Injection-safe by construction. The message is passed on stdin and as
$GODOZO_MESSAGE, never interpolated into the command string — so message text can't break out and run a shell.
One poller per bot token. Telegram allows a singlegetUpdateslistener per token, so don't runlistenand an approvalgateon the same token at once — use a second bot, or run one mode at a time. (Outboundnotifyalways coexists fine.)
Two things to know
It's stateless. Each text is a fresh ask.mjs run, so there's no memory across messages by default — great for one-shot questions. Want a running conversation? Append each turn to a small JSON file keyed by $GODOZO_FROM and load it at the top of the script. A dozen lines.
Every reply spends. This is an OpenRouter call like any other, so it costs — usually a fraction of a cent on a mini model, more on a frontier one. If you'll lean on it, add the cost receipt (and a gate for the big ones) from the spend-gate post so a chatty week doesn't surprise you.
The nice part
It's the same relay in both directions. The bot that freezes a deploy until you say yes is the bot that answers your questions on the train. One outbound-only connection, one allowlist, one audit log — a private line between you and your models that you actually own. Point it at whatever's best this week; when something better drops next month, it's a one-word change.
← More from the godozo blog