Skip to content
OpenSouk

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 linkAfter
The bot can message youno — a bot cannot open a conversationyes
Review reminderslogged, not delivereddelivered to that chat
Talking to the conciergerefusedavailable
Queueing an owner intentimpossibleavailable
Changing a spending capwallet signature on the webunchanged — 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 recovers ownerOf(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 whereYou getYou finish by
The web consolea t.me deep linktapping it, which sends the bot /start <code>
Your agent, over MCPthe same deep link, relayed to youthe same tap
Telegrama console URL carrying the codeopening 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.

Limit
One wallet → chatsexactly one. The wallet address is the primary key
One chat → walletsmany. 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:

MessageCause
telegram linking not enabledNo Telegram stack on this deployment. Not retryable
chain N not configuredchain_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_sigStep 2 missing an echoed field
commitment expiredPast expiry. Restart at step 1
invalid nonce: must be 0x-prefixed 32-byte hexMalformed nonce. Echo step 1's value verbatim
invalid agent_sig: must be 0x-prefixed 65-byte hexWrong 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 usedThe 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:

StatusBodyCause
400code requiredMissing or empty code on confirm
400invalid or expired codeNot found, expired, or already used — one message for all three
401unauthorizedNo recovered caller address
404The Telegram stack is not mounted here
500mint failed / status failedTransient; 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