Bridge — a separate service in the same docker compose assembly as the rest of the core. It knows nothing about agents. It can do three things: fetch an event from MAX, convert it into an internal message, and send a response back.

MAX Bot API: what is needed

The bot works on the official MAX Bot API (platform-api.max.ru). The bot is registered through the MAX partner account.

Events are fetched using long-polling, without webhooks. For one bot it's simpler: no need for a public HTTPS endpoint and signature parsing, and the delay is acceptable. The token is passed in the Authorization header, not in the URL, as is customary in Telegram.

From all types of events, we actually need two:

  • bot_started — the user clicked "Start", including via a link with a start parameter;

  • message_created — a regular message: text, voice, photo, file.

Simplified, the bridge's cycle looks like this (pseudo-code, method names are conditional):

async def poll_loop(api, core): marker = None while True: batch = await api.get_updates(marker=marker, timeout=30) for upd in batch.updates: try: await handle(upd, core) except Exception: log.exception("max: update failed") # one broken update doesn't break the cycle marker = batch.marker async def handle(upd, core): if upd.type == "bot_started": uid = core.identity("max", upd.user.user_id) core.set_first_ref(uid, upd.payload) # source marker await core.on_start(uid, chat_id=upd.chat_id) elif upd.type == "message_created": msg = normalize(upd.message) # text / voice / attachments uid = core.identity("max", msg.sender_id) async for chunk in core.process_message_stream(uid, msg): await send_to_max(msg.chat_id, chunk)

The key here is process_message_stream. It's the same function that the Telegram adapter calls. The entire agent pipeline, memory, and model routing in MAX operates without a single line of MAX-specific code.

Identity

The internal user ID for a guest from MAX is built from their MAX ID. For cases when a single person is logged into both messengers and wants a single memory, there's a mapping table: MAX-id → main account. The bridge re-reads it based on file change time, without restart.

What the MAX Bot API does not provide is the phone number. Therefore, you cannot automatically merge accounts by number. If a person reports an issue and mentions the number, you can only find their request by time and name.

Access: from manual approval to automatic

In the first few months, the bot in MAX was closed. New users ended up in pending, and I received an alert in Telegram with inline buttons «Approve» and «Reject». The main Telegram bot processed the button, and upon approval, the user received a message back to MAX.

The scheme works as long as there's only one user. Pitfalls we encountered: The alert might not have been sent, and an empty error remained in the log without retry. The person was stuck in pending, and I didn't know they even existed.

  • When the bridge and Telegram bot lived on different hosts, the approve flow failed. Had to keep them together.

Access is now automatic with a daily limit on new users. Both entry paths, bot_started and message_created, go through the same check. So access doesn't depend on which event the person started from: clicking "Start" via a link or finding the bot and writing immediately.

Source tags: start → first_ref

This is the most useful part of the entire integration, and it's marketing, not technical.

Each of our bot links carries a tag: ?start=habr_nov, ?start=vc_nov, tags of advertising groups from Direct, and so on. On the first start, the core writes it into the user's first_ref field. Only on the first start, a subsequent start via another link doesn't overwrite the source.

Pseudocode:

REF_RE = re.compile(r"^[a-z0-9_]{1,48}$") def set_first_ref(uid: int, payload: str | None) -> None: ref = (payload or "").strip().lower() if not REF_RE.match(ref): ref = "direct" # empty or garbage — without a tag db.execute( "UPDATE users SET first_ref = %s WHERE id = %s AND first_ref IS NULL", (ref, uid), )

The format restriction is needed not for aesthetics. The start parameter comes from the user, it can be manually changed, and without validation, anything can come into analytics. At the same time, the short Latin format is compatible with Telegram's restrictions on the start parameter, and the same tag works in both messengers.

Further, any report is GROUP BY first_ref. Exactly this way we found out that the previous article on Habr brought 6 people, on vc.ru 1, and the seeds in Telegram channels 43. And that's exactly how Direct thinks: not by clicks from Metrics, but by those who reached the bot and sent a message.

Diplinks: the most unpleasant place

A link of the form https://max.ru/?start=label opens the web version in a desktop browser. On a phone, it depends on where it was opened.

MAX has its own scheme: max://max.ru/?start=label. It opens the app directly if it's installed. If not, the user sees an error or nothing.

On Android, a more reliable intent link with the app package ru.oneme.app and a fallback to web:

intent://max.ru/?start=label #Intent;scheme=max;package=ru.oneme.app; S.browser_fallback_url=https%3A%2F%2Fmax.ru%2F%3Fstart%3Dlabel;end

Choosing a link for the platform is simplified as follows:

function maxLink(bot, label) { const web = `https://max.ru/${bot}?start=${label}`; const ua = navigator.userAgent; if (/Android/i.test(ua)) { return `intent://max.ru/${bot}?start=${label}` + `#Intent;scheme=max;package=ru.oneme.app;` + `S.browser_fallback_url=${encodeURIComponent(web)};end`; } if (/iPhone|iPad/i.test(ua)) return `max://max.ru/${bot}?start=${label}`; return web; }

The main issue turned out to be not the syntax, but the built-in browsers. In October, we tried VK Ads, and 70% of the landing page traffic came from the VKontakte WebView. From such a WebView, transitioning to a third-party app works poorly; less than 1% of visitors clicked the button. Plus, VK does not accept links to max.ru in their ads. The campaign was shut down.

Yandex Direct in search leads people to a regular mobile browser, and there the deep links work as intended.

Channel and moderation

Liza's channel in MAX (max.ru/channel_lizapetrovna) is created not through the app interface, but through the service bot "Channels in MAX for Business", and also from a legal entity. Separately noted: this is not the same model as in Telegram, where any user can create a channel in a minute.

Moderation of ads is simpler. The ad "Neuro Network in MAX without VPN. Free" was bypassed by Direct. It became the best: 139 users in the first day.

Model routing: DeepSeek for mass, Claude for complex

I discussed model routing in the previous article, here only what changed and how it looks in numbers.

The principle remains the same: the cheap fast model answers by default, and only what fits the expensive model goes through explicit triggers (complex query, long context). Profile agents live with their model from the bylaws.

In the last 30 days, the distribution of responses is as follows:

  • DeepSeek V4.1 Flash — about 5,700 responses, ≈84%;

  • Claude Sonnet 5.5 — 825 responses, ≈12%;

  • the rest — DeepSeek via CLI and vision models.

For a free product, where the daily audience for a week has grown from 13–21 to 82–111 people, this is not a matter of taste: mass flow must be cheap, and expensive models are needed where without them the response would be poor.

The downside we see in live dialogues: a cheap model sometimes answers with an English service "prelude" before the actual task. A new user who comes for an image sees this as a malfunction. Image generation succeeds for the designer in about 87% of cases, the rest are malfunctions of the type "try later". This is our top current technical debt, because the designer turned out to be the main scenario for those coming from MAX.

Why three quarters of new users came from MAX

Advertising impressions from November 2–5:

Impressions

11 178

Clicks

1 293

Spend

6 964 ₽

Came to the bot

416 (MAX ≈310, Telegram ≈110)

Wrote at least once

211

About 17 ₽ per user and about 33 ₽ per writer.

Interestingly, MAX doesn't have cheaper active users: the group "AI in MAX without VPN" costs 38 ₽, "Free — Telegram" costs 34 ₽. The difference is in volume. My explanation, and this is a hypothesis, not a measurement:

  1. There is little competition. In Telegram, there are hundreds of AI bots. In MAX, a person looking for a neural network finds only a few.

  2. The argument falls into the trap. "Without VPN" answers a question that MAX users already have. In Telegram, the same argument doesn't resonate, there it works "for free".

  3. Entry threshold. The bot in MAX opens with one button, without registration and without VPN. Combined with deeplinks, this provides a short path from the ad to the first message.

Pitfalls that cost us the most

Ten days of downtime that no one noticed. During the migration of the core to the new prod, the MAX bridge was launched as a separate Docker compose profile in the new build. The profile was not included. Telegram worked, but the MAX bot remained silent for ten days, and no one noticed this. After that, the bridge had an individual watchdog.

Conclusion: if you have multiple transports to one core, you need to monitor each transport separately. The health of the core says nothing about the health of the bridge.

The landing page consumed the MAX buttons. During the migration of the landing page from SPA to static, the MAX buttons were rendered on the client side and did not appear in the static version. Until this was noticed, there was no MAX traffic from the site. Now the buttons are in static, and with every landing page edit, I check that all entry points are in place.

Onboarding. Long consent and three example buttons at the start. In MAX, out of 161 people who saw the consent, 106 accepted, 14 declined, and 41 left silently. On November 2nd, we left a short invitation and one button. This isn't about MAX, but in MAX it's more noticeable because it's the main traffic flow.

What remains unresolved.

Retention. From the previous advertising wave, October 23–26, about 190 people, 14 returned on the seventh day, which is ~7%. The core group that returns at least two days a week, about 36 people.

Technically, the assistant in MAX works the same as in Telegram. Attracting people to it is cheap. But there are not many reasons to return yet, and this is the next task, no longer about the bridge.

If you want to see

The bot is free. Links with the label of this article, using it I will count how many people came from Habr, and write in the comments.

  • Telegram: https://t.me/LizaPetrovna_bot?start=habr_nov

  • MAX: https://max.ru/id9705235960_3_bot?start=habr_nov

If you also make bots for MAX, write in the comments how you solved deeplinks from embedded browsers. This is still our most fragile spot.