Link Telegram
Bind a chat to a wallet, from any of three directions
Linking binds one owner wallet to one Telegram chat. Once bound, the bot can message you — review reminders, a ping when a purchase confirms — and anything you send it is treated as coming from that wallet's owner.
The binding is a row in our database keyed on the wallet address. It grants nothing on-chain, and it is the precondition for the concierge and the intent queue, which both start from a linked chat.
Nothing is deployed on a public chain yet — see Quickstart.
What you will build
A chat bound to your owner wallet, confirmed from both sides, using whichever of the three entry points fits where you are standing.
What the binding grants
| Before the link | After | |
|---|---|---|
| The bot can message you | no — a bot cannot open a conversation | yes |
| Review reminders | logged, not delivered | delivered to that chat |
| Talking to the concierge | refused | available |
| Queueing an owner intent | impossible | available |
| Changing a spending cap | wallet signature on the web | unchanged — still wallet signature on the web |
Authority is keyed on the chat, not on the person sending. Every message from a linked chat carries that wallet owner's authority, with no per-user check inside the chat — which is why the whole surface refuses group chats, on both the message path and the button path (Use the concierge has the refusals).
The link does not unlock money. No amount of chat access raises a spending cap, signs a payment or publishes a review. Set spending limits is a web route with no path into it from the chat, and every economic action is executed by your own agent with its own key.
Before you start
- The owner wallet. The link is keyed on the address, whether that address arrives from a request signature or from an EIP-712 commitment. Which wallet counts as the owner is on Identity and commitments.
- For the tool route: an agent id and the owner's key in
referrer-signer. The commitment recoversownerOf(agent_id), so a signature from a bound agent wallet is refused. - A private Telegram chat with the bot. The bot username is a deployment setting, so read it off the link you are given rather than assembling one.
The link is chain-agnostic: one row per wallet, and the four /v1 routes at
api.opensouk.ai do not read chain_id at all. chain_id on the tool selects which
chain's registry to check the agent's owner against, not which link to write. All four routes are
mounted conditionally and return 404 where the deployment has no Telegram stack — the same
status a mistyped path returns.
Three ways in, one binding
The code is single-use, and the direction is recorded on it: a code minted for one direction cannot
be consumed in the other. Every code lives ten minutes, which is a deployment setting — read
expires_at off the response rather than assuming it.
| Start where | You get | You finish by |
|---|---|---|
| The web console | a t.me deep link | tapping it, which sends the bot /start <code> |
| Your agent, over MCP | the same deep link, relayed to you | the same tap |
| Telegram | a console URL carrying the code | opening it, connecting your wallet, confirming |
Prompt mode
For the agent-initiated route. The agent mints the link; you tap it.
Show the prompt
Mint me a Telegram link for my OpenSouk agent on Base mainnet. My agent id is 7.
1. Call link_telegram with agent_id and NO agent_sig. It returns a commitment, a nonce and an
expiry one hour out.
2. Call sign_telegram_link_commitment with that commitment object exactly as returned. The
owner key is in the signer — do not ask me to sign in a wallet.
3. Call link_telegram again with the same agent_id plus nonce, expiry and the signature as
agent_sig.
4. Give me the deep_link and the expires_at verbatim. The deep link expires in about ten
minutes, which is much sooner than the commitment did — do not tell me I have an hour.
5. If step 3 fails with "commitment nonce already used", the nonce was consumed. Restart at
step 1 with a fresh commitment rather than retrying the same signature.
6. Do not attempt to complete the link yourself. I tap the deep link in Telegram; nothing you
can call finishes it.Manual mode
Pick a direction
- Web or agent first. We already know your wallet; the code carries that, and tapping the deep link supplies the chat.
- Telegram first. We already know your chat; the code carries that, and confirming on the web supplies the wallet.
Mint from the web
curl -X POST https://api.opensouk.ai/v1/telegram/link/start \
-H "X-Agent-Timestamp: $TS" -H "X-Agent-Signature: $SIG"{
"deep_link": "https://t.me/<bot>?start=Yk9tR2xJc1V…",
"code": "Yk9tR2xJc1V…",
"expires_at": "2026-09-02T18:45:00Z"
}No body, and no address argument. The record key is the wallet recovered from your signature, so there is no shape of request that links somebody else's chat.
code is 32 random bytes as unpadded base64url — 43 characters, inside Telegram's 64-character
deep-link payload limit. Minting again does not revoke the previous code: each mint inserts an
independent code, so every unused one stays redeemable until it expires or is consumed.
Or mint from your agent
link_telegram is the same mint with an EIP-712 proof in place of the request signature.
{ "agent_id": 7 }Step 1 returns status: commitment with the typed data, a 32-byte nonce and an expiry one hour
out. Sign the commitment object with sign_telegram_link_commitment — it takes the whole object
and reads the agent id out of the message to pick the key — then resend:
{ "agent_id": 7, "nonce": "0x6d…", "expiry": 1785…, "agent_sig": "0x…" }{ "status": "linked", "deep_link": "https://t.me/<bot>?start=Yk9tR2xJc1V…",
"expires_at": "2026-09-02T18:45:00Z" }status: linked means the link was minted, not that you are linked. Nothing is bound until the
human taps it. expires_at here is the deep link's ten minutes, not the commitment's hour.
This is the one commitment whose nonce we burn in our own database. Minting a deep link is not idempotent — a replayed signature could mint a second link and reroute the owner's notifications — so the nonce is consumed after the signature verifies and before the mint. The burn keys on the canonical lowercase form re-encoded from the decoded bytes, so re-casing your echo does not slip past it. What each commitment binds is on Identity and commitments.
Or start in Telegram
Send the bot a bare /start, with no code. It answers with a console URL carrying a code, in the
opposite direction:
Finish linking your wallet here: https://<console>/settings/telegram?code=NEs4bTJxWnA…A still-valid pending code is reused rather than replaced, so sending /start twice does not
invalidate the URL you already have. Open it, connect the wallet, and confirm:
curl -X POST https://api.opensouk.ai/v1/telegram/link/confirm \
-H 'Content-Type: application/json' \
-H "X-Agent-Timestamp: $TS" -H "X-Agent-Signature: $SIG" \
-d '{"code":"NEs4bTJxWnA…"}'If the chat is already linked, a bare /start says so instead of minting: one wallet is named, and
several are counted with a pointer to /status.
Finish the web-first direction in Telegram
Tap the deep link, or send the bot /start <code> by hand. It replies with the wallet it bound:
✅ Linked to 0x…Consuming the code and writing the link are one transaction: if the bind fails, the consume rolls back and the code stays redeemable rather than being burned with nothing to show for it.
Expired, already used and never existed are deliberately one message, so the bot cannot be used to test whether a code exists:
That link is invalid or expired. Start again from the console.Verify
curl -X POST https://api.opensouk.ai/v1/telegram/link/status \
-H "X-Agent-Timestamp: $TS" -H "X-Agent-Signature: $SIG"{ "linked": true, "telegram_username": "ada_owner", "linked_at": "2026-08-28T14:03:21Z" }The status read is a POST. All four Telegram routes are, so one signing scheme covers them
identically and none of them takes an address argument.
telegram_username is best-effort and display-only. It can be missing from a successful confirm
response without meaning anything failed: the bind has succeeded and the code is burned by the time
that lookup runs.
From the chat side, /status lists what this chat is linked to, and /help prints the current
command set.
How many links you can have
| Limit | |
|---|---|
| One wallet → chats | exactly one. The wallet address is the primary key |
| One chat → wallets | many. A person can link several wallets to the same chat |
Linking a wallet that is already linked reroutes it. The write upserts on the address: the new chat replaces the old one, no error is raised, nothing records that the previous binding existed, and that wallet's notifications start arriving somewhere else.
A multi-wallet chat makes you name which wallet you mean. The concierge refuses an ambiguous
message and asks you to start it with an address; /unlink refuses a bare invocation and wants an
address or a unique prefix of one.
Unlinking
Either side, and both are a hard delete of the row rather than a flag.
curl -X POST https://api.opensouk.ai/v1/telegram/link/unlink \
-H "X-Agent-Timestamp: $TS" -H "X-Agent-Signature: $SIG"{ "linked": false }That body comes back whether or not there was a link to remove, so the call is idempotent and
tells you nothing about the prior state. Read status first if you need to know. From the chat,
/unlink does the same and confirms which wallet stopped:
Unlinked 0x…. You will no longer receive its notifications.Unlinking is also the opt-out for notifications: there are no per-type preferences to turn a single kind of message off.
What a linked chat starts sending you
Two proactive kinds today, both on a closed whitelist that exists to keep the channel from becoming a promotional one.
Review reminders, on the schedule for a purchase awaiting your review.
A purchase ping, when a purchase of yours confirms:
🧾 Purchase confirmed: <product> (escrow #N).
Tell me what you thought of it and I'll draft your review.
Rather do it later? Send /snooze N 5d and I'll remind you then.The ping is skipped, not failed, for an agent-less purchase — there is no owner to route it to until the escrow is bound (Buy agent-less and bind later) — and it is sent once: a durable marker is written before the send, so a replayed indexer pass never re-pings.
/snooze <escrow_id> <duration> defers the review reminder for that one purchase and leaves
every other purchase on its normal schedule. It covers the buyer-review reminder only, it will not
push past the escrow's 60-day settlement window, and it answers one message for every refusal —
unknown escrow, another owner's escrow, an unbound purchase — for the same anti-enumeration reason
the invalid-code reply is one message.
Errors and retries
From the tool:
| Message | Cause |
|---|---|
telegram linking not enabled | No Telegram stack on this deployment. Not retryable |
chain N not configured | chain_id names a chain this server does not run |
agent_id must not be negative: … | A negative agent_id. Zero is valid |
nonce and expiry are required with agent_sig | Step 2 missing an echoed field |
commitment expired | Past expiry. Restart at step 1 |
invalid nonce: must be 0x-prefixed 32-byte hex | Malformed nonce. Echo step 1's value verbatim |
invalid agent_sig: must be 0x-prefixed 65-byte hex | Wrong signature length, or not hex |
signature not from agent owner: recovered 0x…, want 0x… | The recovered signer is not ownerOf(agent_id) |
commitment nonce already used | The nonce was burned. Restart at step 1 |
agent N not found on-chain: … | ownerOf reverted. Wrong id, or wrong network |
mint telegram deep link: … | The mint failed after the nonce was burned. Restart at step 1 |
From the routes:
| Status | Body | Cause |
|---|---|---|
400 | code required | Missing or empty code on confirm |
400 | invalid or expired code | Not found, expired, or already used — one message for all three |
401 | unauthorized | No recovered caller address |
404 | — | The Telegram stack is not mounted here |
500 | mint failed / status failed | Transient; retry |
A failed mint after a burn costs you the nonce and nothing else. Nothing was bound, so a clean step 1 is the whole recovery.
Next steps
- Telegram and limit routes — all four routes, field by field
- Telegram and intent tools — the tool twin
- Settings and simulators — the console screen for both directions