> ## Documentation Index
> Fetch the complete documentation index at: https://docs.splox.io/llms.txt
> Use this file to discover all available pages before exploring further.

# A Telegram bot

> Ask for a bot that answers with your agent — and read the one failure that catches every program on this platform before you write your own

A bot on Splox is not an integration you switch on. It is a
[program](/concepts/program) in your harness — a loop that polls Telegram and
hands each message to your agent — and you get it by asking for it. It runs as
long as the machine does, and it answers with the same agent, on the same
machine, that you talk to in a chat.

This page also carries the one failure worth knowing about before you ask for any
program at all. It cost a working bot a full day of never answering anybody.

## The part you have to do yourself

**Make the bot.** Message [@BotFather](https://t.me/botfather) on Telegram, send
`/newbot`, answer two questions, and it hands you a token that looks like
`123456789:AAH...`.

This is one of the very few steps in these tutorials your agent cannot do for
you: it needs a Telegram account, which needs a phone number, which is a person.
A minute of your time, once.

**Put the token where the machine can read it.** Open **Connections → Secrets**
and add it as `TELEGRAM_BOT_TOKEN`.

<Frame caption="Environment secrets are injected into the machine's shells; the value is never shown again">
  <img src="https://mintcdn.com/sploxltd-165e0515/FtagtnY5r9E1DKmP/images/tutorials/telegram-secret.png?fit=max&auto=format&n=FtagtnY5r9E1DKmP&q=85&s=a949f91d5eaf5e432e6fa416325d356a" alt="The Connections Secrets tab with TELEGRAM_BOT_TOKEN saved" width="2880" height="1800" data-path="images/tutorials/telegram-secret.png" />
</Frame>

Secrets are encrypted key/value pairs injected as environment variables into the
sandbox, for every chat. Nothing writes a token into a file in git. See
[Connections](/app/connections).

## What you say

Then it is one message:

```text theme={null}
I have a Telegram bot token from BotFather, and it is already in this machine's
secrets as TELEGRAM_BOT_TOKEN. I want messages sent to that bot answered by you,
on this machine, with each person keeping one conversation instead of starting a
new one every time. There is an old telegram program in the harness from an
earlier experiment — replace it. Publish it, start it, and tell me it is polling.
```

Nothing in that message is about Python. The three things worth naming are the
ones a bot is wrong without: **where the token is**, **who answers**, and **that
one person keeps one conversation**.

## The one line that matters: who answers

The old program in the harness declared a bot-only agent of its own:

```python theme={null}
support = agent(
    "Support",
    system_prompt=(
        "You answer questions from Telegram, on the machine you are running on. "
        "One short paragraph, no preamble, no headings."
    ),
    model="kimi-k3",
    tools=["system:compute"],
)
```

That works, and it is almost always not what you want. A private agent declared
inside a bot has its own prompt, its own tool list, and none of the things your
actual assistant has grown — the memory, the custom tools, the hooks. You get a
second, dimmer version of your agent living behind a Telegram handle.

Asked for messages "answered by **you**", the agent deleted its own throwaway
Support and delegated instead:

<Accordion title="Delegating to the agent the chat talks to">
  ```python theme={null}
  from splox import program

  assistant = program("splox").assistant

  run = assistant(text, chat_id=chats.get(chat), wait=True)
  reply, said_in = run.output(), run.chat_id
  chats[chat] = said_in
  ```

  `program("splox").assistant` hands back the very agent declared in
  `programs/splox/main.py` — the one a chat in the app reaches, with its tools, its
  skills and its hooks. The program is the delivery loop; the harness is the brain.

  `chat_id` is what makes one Telegram chat one conversation: the run's `.chat_id`
  is stored per person and handed back on their next message, so they continue
  instead of starting over. The map and the update offset live in a JSON file on
  the machine, so a restart does not lose anybody's thread or answer a message
  twice.
</Accordion>

<Frame caption="Version 10: the throwaway agent gone, the loop delegating, and two publish rules learned the hard way">
  <img src="https://mintcdn.com/sploxltd-165e0515/FtagtnY5r9E1DKmP/images/tutorials/telegram-answer.png?fit=max&auto=format&n=FtagtnY5r9E1DKmP&q=85&s=f9a596f44311e2d118e9a43597530a7c" alt="A Splox chat where the agent replaces the bot's private agent with the harness Assistant and publishes version 10" width="2560" height="1600" data-path="images/tutorials/telegram-answer.png" />
</Frame>

Two things the push taught, both worth knowing in advance: a stray
`__pycache__/*.pyc` will get committed if you are not looking, and the publish
gate **refuses a root `.gitignore`** — the tree takes programs, tools, evals and
root documents, so an ignore file belongs inside the program directory, where a
program may be any shape it likes.

## The failure worth reading

An earlier version of this bot was published, started, and printed
`polling as @…` to its log. Everything looked right. It had never answered a
single message, and the state file that would have proved otherwise did not
exist.

The platform's error was this:

```text theme={null}
programs/telegram could not say who 'Support' is: KeyError: 'TELEGRAM_BOT_TOKEN'
```

The cause is a single line in the wrong place. The program read the token at the
top of the file, next to the imports:

```python theme={null}
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]   # at import time — wrong
```

Your `main.py` is not only run as a process. The platform **imports** it whenever
it needs to ask who an agent is — and that import happens in a context that does
not carry your machine's secrets. So the loop ran fine, took a message, asked the
platform to run its agent, and the platform could not import the file to find out
who that agent was. The bot polled forever and answered nobody.

The fix is to read the secret where it is used, and the comment the agent left
behind says why better than a rule would:

```python theme={null}
def call(method, **params):
    """One Bot API call.

    The token is read here rather than at import time on purpose: the platform
    imports this file whenever it needs to know who "Support" is, and that
    import does not carry the machine's environment secrets.
    """
    token = os.environ["TELEGRAM_BOT_TOKEN"]
```

<Warning>
  **Never read a secret at import time in a program's `main.py`.** Read it inside
  the function that uses it. This applies to every program, not only bots — the
  same import happens for a nightly job, a webhook, a mailbox watcher. The symptom
  is nasty because the loop itself keeps running and logging happily.
</Warning>

## Checking on it

Ask your agent. It is on the machine the bot runs on, so "is the bot polling, and
has anybody messaged it?" is answered by looking: the process, `/tmp/telegram.log`,
and the state file with one entry per person.

<Note>
  What is not shown on this page is a screenshot of Telegram itself, because the
  account behind this bot belongs to a person and these tutorials were driven by
  asking rather than by tapping a phone. The publish, the delegation and the
  polling are all shown above; the last hop is the Bot API's, and it is the part
  you will see first.
</Note>

## Next

<CardGroup cols={2}>
  <Card title="Patterns" icon="shapes" href="/reference/patterns">
    A router, a nightly job and a fan-out over many items, in the same shape as this one.
  </Card>

  <Card title="Programs" icon="folder" href="/reference/programs">
    What the platform reads out of a program, what it never opens, and how a program calls its agents.
  </Card>
</CardGroup>
