Oslo Vibe Coding · Session 07 · 10 September 2026
Build your first agent, for free.
By the end of this page you will have a Telegram bot, running on the internet, that answers you, checks live Oslo departures and weather, remembers what you tell it, and messages you first when a reminder is due. It costs nothing and needs no API key and no card.
Open the session deck Short version (README)- What you are building, and what "agent" means here
- Before you start (3 minutes)
- Part 1: make the Telegram bot
- Part 2: make the Val.town project and paste the six files
- Part 3: give it the bot token
- Part 4: turn on the HTTP trigger and connect Telegram
- Part 5: test it, one layer at a time
- Part 6: the cron, where it becomes an agent
- What happens when you send a message
- When something does not work
- Make it yours, and the next two sessions
What you are buildingand what "agent" means here
A chatbot takes a message and returns a message. An agent has three more things: tools it can decide to use, memory that survives between conversations, and a trigger that is not a person, so it can act on its own. We add those in that order, and stop to test after each one.
| Layer | File | What it adds |
|---|---|---|
| 1. Chat | model.ts, telegram.ts | Telegram sends your message to a URL; a model answers; the answer goes back. A chatbot. |
| 2. Tools | tools.ts | Two functions the model may ask for: live departures from any Oslo stop (Entur) and the hourly forecast (Yr). The model chooses when. |
| 3. Memory | memory.ts | A small database. The last 20 messages, facts you told it, and reminders. |
| 4. Autonomy | reminders.ts | A timer wakes the code every 15 minutes. If a reminder is due, it messages you. Nobody asked. |
The piece that ties them together is agent.ts, forty lines: ask the model, run the tool it asks for, show it the result, repeat, stop when it answers or after five tool calls. Read that file once. Everything else is plumbing.
Why it is free
| Piece | Service | Free limit today |
|---|---|---|
| Where the code runs | Val.town, free plan | 100,000 runs a day, a database of 10 MB, timers down to every 15 minutes |
| The model | Val.town's built-in model proxy | Free accounts get the cheap models (gpt-5.4-nano). No key to create. |
| Departures | Entur, Norway's national transport data | Free and open; you only have to name your app in a header |
| Weather | Yr (MET Norway) | Free and open; same rule, name your app |
| The chat | Telegram Bot API | Free, unlimited bots |
Fallback If the built-in model ever stops working, get a free key at openrouter.ai/keys, add it as OPENROUTER_API_KEY, and the code switches to OpenRouter's free models by itself. Those allow 50 requests a day.
Before you start3 minutes
- Telegram on your phone or laptop, signed in. If you do not have it: telegram.org, it is free.
- A Val.town account. Go to val.town, click Sign Up, sign in with GitHub or Google or an email. No card is asked for.
- This page open next to Val.town, so you can copy from here.
Part 1Make the Telegram bot
1.1Open BotFather
In Telegram, search for BotFather (the one with a blue verified tick) and open the chat. Press Start.
What this is BotFather is Telegram's own bot for creating bots. Every bot on Telegram was born here.
1.2Create the bot
Send /newbot. It asks for a name (anything, e.g. My Oslo Assistant), then a username, which must be unique and end in bot, e.g. kari_oslo_bot.
You should see "Done! Congratulations on your new bot" and a line that says Use this token to access the HTTP API: followed by a long string like 7123456789:AAF...
Keep it secret That token is the bot's password. Anyone who has it can send messages as your bot. Do not paste it in a group chat or a screenshot. We will store it in one place only, in Part 3.
Part 2Make the Val.town project and paste the six files
2.1Create a project
Signed in at val.town, click New (top left or top right depending on the layout) and choose Project. Name it oslo-assistant. A project is just a folder of files with one URL each.
You should see an empty project with a file list on the left and an editor on the right.
2.2Add the six files
For each file below: click the + (new file) in the file list, type the exact name, open the link here, select all, copy, and paste into the editor. Val.town saves as you type.
- agent.tsThe loop. No Val.town-specific code; you could run it anywhere.
- tools.tsLayer 2. Entur departures and Yr weather.
- memory.tsLayer 3. Creates three tables the first time it runs.
- model.tsLayer 1. Picks the built-in model, or OpenRouter if you add a key.
- telegram.tsThe front door. Telegram posts each message here.
- reminders.tsLayer 4. Runs on a timer.
You should see six files in the list. Red underlines in telegram.ts or reminders.ts before the others exist are normal; they go away once every file is in.
Why six files and not one Val.town attaches one trigger (HTTP, cron, email) per file. The Telegram door and the timer are different triggers, so they are different files. The rest are split so the deck can point at one file per layer.
Part 3Give it the bot token
3.1Add an environment variable
In the project, open Settings (gear icon or the project menu) and find Environment variables. Add one: name TELEGRAM_BOT_TOKEN, value = the token BotFather gave you. Save.
What this does The code reads the token with Deno.env.get("TELEGRAM_BOT_TOKEN"). It lives in Val.town's secret store, not in a file, so it never appears in your code, in a screenshot, or in your project's public view.
Part 4Turn on the HTTP trigger and connect Telegram
4.1Make telegram.ts an HTTP val
Open telegram.ts. Next to the file name there is a trigger selector (it may say Script). Set it to HTTP.
You should see a URL appear for the file, shaped like https://yourname--telegram.web.val.run. Copy it.
What this means The file is now a tiny web server. Every request to that URL runs the file's default function once, with the request as the argument.
4.2Open the URL once with ?setup
In a new browser tab, open your val's URL with ?setup on the end:
https://yourname--telegram.web.val.run?setup
You should see JSON with "ok": true and your URL under webhook.
What just happened Your val called Telegram's setWebhook API and said "from now on, send every message for my bot to this URL". Telegram now knows where your bot lives. The token stayed on the server; you never had to paste it into a URL.
If you see "Missing TELEGRAM_BOT_TOKEN", Part 3 did not save. If you see "ok": false with "unauthorized", the token has a typo.
Part 5Test it, one layer at a time
5.1Layer 1, chat: say hello
In Telegram, open your bot (search for its username) and press Start. Send: hello, who are you?
You should see a reply within a few seconds. In Val.town, open the Logs panel for telegram.ts; each message shows up as one request.
What happened Telegram POSTed your message to the val. The val put your text after a system prompt, sent it to the model, and sent the model's text back through Telegram's sendMessage. A chatbot. No tools were needed for "hello", so none were used.
5.2Layer 2, tools: ask something it cannot know
Send: What is the weather like in Oslo this afternoon? Then: When is the next tram from Jernbanetorget?
You should see real numbers: temperatures per hour, and departures with line numbers and times. Not "I do not have access to real-time data".
What happened The model was told, in tools.ts, that two functions exist and what they do. Instead of answering, it replied with "call oslo_weather". The loop in agent.ts ran the function, which fetched Yr, and put the result back in front of the model. Then the model wrote the answer. You decided nothing about when to check the weather; the description of the tool did.
5.3Watch it choose two tools by itself
Send: I'm at Jernbanetorget going to Majorstuen. Should I leave now?
You should see one recommendation with a departure time and a word on the weather. Two tool calls happened for one question.
5.4Layer 3, memory: tell it something about yourself
Send: My home stop is Majorstuen and I hate cycling in the rain. Then, as a new message: Should I leave now?
You should see it use Majorstuen without being told again.
What happened remember, recall and set_reminder are just three more tools, backed by a SQLite database Val.town gives every account. The model used remember because you said something about yourself, then recall before answering the second question. Nobody wrote code that decides when to remember; the tool description did that work.
Part 6The cron, where it becomes an agent
6.1Make reminders.ts a cron val
Open reminders.ts, set its trigger to Cron, and set the schedule to every 15 minutes: */15 * * * *. (15 minutes is the shortest interval on the free plan.)
6.2Ask for a reminder, then wait
Send: Remind me in 10 minutes to drink water.
You should see a confirmation with the exact time. Then, within 15 minutes of that time, a message that starts with Reminder: arrives. You did not send anything. The agent acted on its own schedule.
What happened set_reminder wrote a row into the reminders table. Every 15 minutes Val.town runs reminders.ts, which asks the table for rows whose time has passed and have not been sent, sends each one through Telegram, and marks them sent. That is the whole difference between a chatbot and an agent: a trigger that is not a person.
What happens when you send a messagethe whole path, in order
- You type in Telegram. Telegram's servers see your bot has a webhook and POST the message as JSON to your val's URL.
telegram.tsruns. It saves your message to themessagestable and loads the last 20 messages for your chat, so the model has the conversation.- It builds the message list: a system prompt with today's date and Oslo time, then the history.
agent.tssends that plus the five tool descriptions to the model.- The model replies either with text (done) or with "call this tool with these arguments".
- If a tool: the loop runs the function, appends the result as a
toolmessage, and goes back to step 4. At most five times. - The final text is saved to
messagesand sent to you with Telegram'ssendMessage. The val returns 200 so Telegram stops retrying. - Separately, every 15 minutes,
reminders.tswakes up and sends whatever is due.
The guardrails, on purpose
- Five tool calls maximum. After that it answers "I could not finish within 5 tool calls" and lists what it tried. Autonomy without a stop condition is just an expensive while-loop.
- A failing tool returns an error string to the model instead of crashing the request, so the model can say what went wrong.
- The tools list is the permission list. The model cannot do anything that is not one of those five functions. It cannot browse, delete, or pay.
- The token is a secret, never in code.
When something does not work
| What you see | What it means | Do this |
|---|---|---|
| Bot never replies | Telegram does not know where the val is, or the val errors | Open the val URL with ?status. If url is empty, run ?setup again. If last_error_message is set, read it; then open Logs in Val.town. |
| "Missing TELEGRAM_BOT_TOKEN" | The environment variable is not saved or has a different name | Settings, Environment variables, check the name exactly. |
?setup says unauthorized | Token typo | Copy it again from BotFather; /token in BotFather shows it. |
| Reply says it cannot access live data | The model did not use the tool | Ask more concretely: name the stop. Check tools.ts pasted fully; the toolSchemas block at the bottom is what the model reads. |
| "No stop found" | Entur's search did not match | Use the official stop name, e.g. Jernbanetorget, Majorstuen, Nationaltheatret. |
| Reminder never arrives | Cron not set, or time parsed wrong | Check the trigger on reminders.ts is Cron with */15 * * * *. Ask with a clear time: "at 08:00 tomorrow". |
| Model errors or rate limits | The built-in proxy is busy or has changed | Get a free key at openrouter.ai/keys, add OPENROUTER_API_KEY in Environment variables. The code switches by itself. |
| Red underlines in the editor | A file the code imports is missing or misnamed | Names must match exactly: agent.ts, tools.ts, memory.ts, model.ts, telegram.ts, reminders.ts. |
Make it yoursand the next two sessions
Twenty minutes, pick one
- A third tool. Oslo Bysykkel publishes live station status with no key (oslobysykkel.no/apne-data). Open Claude or ChatGPT next to Val.town, paste
tools.ts, and ask for abike_stationstool in the same shape. Read what it writes before you paste it. - Norwegian. Change the system prompt in
telegram.tsto answer in Norwegian. - A morning briefing. A second cron at 07:45 that runs the agent with the question "should I leave now?" and messages you the answer.
Same project, two more Wednesdays
- Give it an email address. Val.town can run a file when mail arrives at
something@valtown.email. Forward it a newsletter or a receipt; it summarises, pulls out dates, files to memory, and sends a weekly digest. On the free plan it can only email you, which is all we need. - Give it eyes. A watchdog: hand it a URL and a question. Every hour it fetches the page, compares with what it remembers, and messages only when the answer changed.
Running the loop tests on your own laptop, if you have Node 22.18 or newer: download agent.test.ts next to agent.ts and run node --test agent.test.ts.