Compass · Instagram DM bot

Full DM catalog — current vs ideal

Every Instagram DM the bot sends, what it looked like before, and what it sends now (shipped 2026-06-05). Two-bubble result DMs are a platform constraint, not a style choice — see section 2b.

Implemented · on main · not yet committed

1 · The problem in one line

A single shared reel used to trigger 5-6 message bubbles: pulling photos → places → insight → milestone → new-country → nudge. Each "rich" DM also split into a text bubble plus a separate button bubble, so the real count was even higher.

2 · The shape now (the rule)

Every save resolves to at most three logical DMs, in order:

  1. Instant ack — fired the moment the share lands, before extraction. Got it - pulling the travel spots from this one. One sec (reel) / Got it - let me pull all the photos. One sec (carousel).
  2. Result DM — exactly one. Its text bubble leads with a one-sentence summary, a blank line, then the places list. The action button is its own separate bubble (section 2b).
  3. Moment DM — at most one, only when warranted. A single priority pick (milestone > new-country > nudge). The old insight DM is gone — that one-liner now leads the Result DM as the summary, so it never costs its own bubble.
The summary line costs $0. It rides the extraction Gemini call we already pay for: the slow path reuses the V8 insight field; the fast (webhook) path added a summary field to the caption-only call. Null → the bubble just opens with the header.

Voice = sentence case, terser. First word of a sentence and proper nouns stay capitalized (Compass, Instagram, country names). We did not lowercase everything — only tightened copy and dropped Title-Case button labels to sentence case. Re-shares get the Result DM only (no ack, no moment). Transport is unchanged — we cut the logical DM count, not the per-message text-then-button split.

2b · How an Instagram DM actually renders

A plain text message carries the long content (summary + header + places) up to ~1000 chars, but cannot have a tappable button. A "special" message (button / generic template) is the only way to attach a tappable web_url button, but its text is an ~80-char title — it cannot hold the long places list.

So the button is always its own bubble. A Result DM is physically two bubbles: the plain-text list, then a separate special message carrying just the CTA. Below, the dashed bubble labelled special message is that detached CTA — never part of the text bubble above it.

3 · Root cause — three uncoordinated layers (before)

LayerFileWhat it sent
Webhook (instant)webhooks.routes.ts carousel pulling photos ack; fast places DM (sets webhookDmSent); acks; dup replies; OTP / greeting / quick-reply / guest-cap
Consumer (~5-45s)content-consumer.ts the fan: places → insight (+500ms) → milestone (+1s) → new-country (+1s) → nudge (+2s). Follow-ups were not gated by webhookDmSent, so they fired even after the webhook already replied
Transportinstagram.service.ts text bubble, wait 500ms, then a separate button bubble — doubles every rich DM

4 · Before / after count (logical DMs)

CaseBeforeNow
Plain reel, places found1-2 (+ up to 4 more)2 ack + result
Reel + new countryup to 43 ack + result + moment
Reel + milestoneup to 43
Reel + country + milestone + nudgeup to 63 (one moment wins)
First save ever2-43
Itinerary reel2-52-3
Carousel2-62 ack + result
Carousel + milestoneup to 63 (ceiling)
Same-user re-share11 result only
Cross-user cache hit1-41
Bookmark / failure / reject11

Part A · The save-flow cases

Reading the mockups: dark bubbles are bot DMs (text). The brass dashed bubble is the CTA, delivered as a separate "special message" (section 2b). The italic brass line at the top of a result bubble is the $0 summary. Bubble counts are honest (text + special each count).

A1 · Plain reel, 1-5 spots, fast path 2 DMs

Beforeup to 5 logical DMs

instantFound them ๐Ÿ‘‡ ๐Ÿ‡ฎ๐Ÿ‡ฉ India ๐Ÿ–๏ธ Padang Padang Beach ๐Ÿœ Warung Bu Mil + 2 more All saved to your collection.
special message๐Ÿ“‚ My Collection
+500ms(insight)
+1s(milestone)
+2s(app nudge)

Now2 DMs · 3 bubbles

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · resultA laid-back guide to Bali's southern beaches and surf breaks.๐Ÿ‡ฎ๐Ÿ‡ฉ Bali ๐Ÿ–๏ธ Padang Padang Beach ๐Ÿœ Warung Bu Mil ๐Ÿ›• Uluwatu Temple ๐ŸŒ… Single Fin Bar Saved to your collection.
special message๐Ÿ“‚ Bali picks

Now: 2 logical DMs (ack + result) → 3 visible bubbles.

A2 · Plain reel, 6+ spots (teaser) 2 DMs

Now2 DMs · 3 bubbles

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · resultA hit-list of Bali's hidden beaches, north to south.๐Ÿ‡ฎ๐Ÿ‡ฉ Bali ยท 9 spots found ๐Ÿ–๏ธ Padang Padang Beach ๐Ÿœ Warung Bu Mil ๐Ÿ›• Uluwatu Temple ๐ŸŒ… Single Fin Bar + 5 more in the app
special message๐Ÿ“‚ Bali picks

A3 · Reel into a new country 3 DMs

Now3 DMs · 5 bubbles

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · resultA first taste of Lisbon - trams, miradouros, and pastรฉis.๐Ÿ‡ต๐Ÿ‡น Portugal ๐Ÿฝ๏ธ Time Out Market ๐Ÿš‹ Tram 28 ๐Ÿฐ Sรฃo Jorge Castle ๐Ÿฅ Manteigaria Saved to your collection.
special message๐Ÿ“‚ Portugal picks
~30s · moment๐Ÿ‡ต๐Ÿ‡น That's country #12 in your collection.
special message๐Ÿ—บ๏ธ Plan Portugal

The new-country line is the single Moment DM — milestone > new-country > nudge, one wins.

A4 · Reel lands on a save milestone 3 DMs

Now3 DMs

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · resultA weekend of Bali's best sunset spots.๐Ÿ‡ฎ๐Ÿ‡ฉ Bali โ€ฆ (spots) โ€ฆ Saved to your collection.
special message๐Ÿ“‚ Bali picks
~30s · moment25 spots across 12 countries. Your itinerary builder has serious material to work with.
special message๐Ÿ—บ๏ธ Plan a trip

A5 · First save ever 3 DMs

Nowwelcome moment

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · resultA first run at Bangkok - street food and temples.๐Ÿ‡น๐Ÿ‡ญ Bangkok ๐Ÿœ Jay Fai ๐Ÿ›บ Wat Arun ๐ŸŒƒ Chinatown Yaowarat Saved to your collection.
special message๐Ÿ“‚ Bangkok picks
~30s · momentYour first spot is saved ๐ŸŒ This is where your collection begins.
special message๐Ÿ“‚ Bangkok picks

Guests get โœจ Open Compass (reveal token) spliced into the CTA instead of the bare collection URL.

A6 · Itinerary reel (day-by-day) 2-3 DMs

Now2 DMs · CTA = 2 buttons in one special msg

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · resultThree days in Tuscany, from Florence's galleries to the Chianti hills.๐Ÿ‡ฎ๐Ÿ‡น Italy ยท 7-day itinerary ๐Ÿ“… Day 1 ยท Rome ๐Ÿ“ Colosseum ๐Ÿ“ Trevi Fountain ๐Ÿ“… Day 2 ยท Florence ๐Ÿ“ Uffizi Gallery ๐Ÿ“ Duomo + 5 more days in the app
special message๐Ÿ“‚ Italy picks  ·  ๐Ÿ“… Day-by-day

A7 · Destinations-only reel 2 DMs

Now"5 places to visit in Italy"

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~2s · result๐Ÿ‡ฎ๐Ÿ‡น Italy ยท 5 destinations to visit ๐Ÿ“ Rome ๐Ÿ“ Florence ๐Ÿ“ Venice ๐Ÿ“ Amalfi Coast ๐Ÿ“ Cinque Terre Saved to your bucket list.
special message๐Ÿ“‚ Italy picks

A8 · Slow path (thin caption) 2 DMs

Nowack instant, result ~30s later (summary = V8 insight)

instant ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
~30s · resultA Tokyo highlights reel - sushi, shrines, and neon.๐Ÿ‡ฏ๐Ÿ‡ต Tokyo ยท 6 spots found ๐Ÿฃ Sushi Dai โ›ฉ๏ธ Senso-ji ๐ŸŒƒ Shibuya Crossing + 3 more in the app
special message๐Ÿ“‚ Tokyo picks

A9 · Carousel success 2 DMs

Nowcarousel ack already existed — same shape

instant ackGot it - let me pull all the photos. One sec ๐Ÿ“ธ
~15-30s · resultA grand tour of Italy's classic cities.๐Ÿ‡ฎ๐Ÿ‡น Italy ยท 9 spots found ๐Ÿ“ Rome โ†’ Colosseum, Trevi, Pantheon ๐Ÿ“ Florence โ†’ Uffizi, Duomo + 4 more across Venice, Amalfi
special message๐Ÿ“‚ Italy picks

A10 · Carousel + milestone 3 DMs · ceiling

Nowthe maximum any save reaches

instant ackGot it - let me pull all the photos. One sec ๐Ÿ“ธ
~20s · resultA grand tour of Italy's classic cities.๐Ÿ‡ฎ๐Ÿ‡น Italy ยท 9 spots found โ€ฆ
special message๐Ÿ“‚ Italy picks
moment50 spots. You're building a proper travel guide at this point.
special message๐Ÿ—บ๏ธ Plan a trip

A11-A12 · Carousel can't resolve 2 DMs

A11 · caption gave โ‰ฅ2 spotstext only

instant ackGot it - let me pull all the photos. One sec ๐Ÿ“ธ
later๐Ÿ‡ฎ๐Ÿ‡น Got 3 spots from the caption โ˜‘๏ธ Instagram didn't share the full carousel so I couldn't grab the rest. Paste the link if you want me to dig deeper.

A12 · nothing usabletext only

instant ackGot it - let me pull all the photos. One sec ๐Ÿ“ธ
laterHmm, Instagram didn't share the full carousel with me. Paste the reel link here and I'll handle it ๐Ÿ‘‡

These two are plain text with no button — a single bubble each, no special message.

A13-A14 · Re-shares 1 DM

A13 · same-user re-share1 DM · no ack, no moment

Already in your collection ๐Ÿ”ฅ (saved 3 days ago) ๐Ÿ‡ฎ๐Ÿ‡ฉ Bali ยท 4 spots ๐Ÿ–๏ธ Padang Padang Beach ๐Ÿœ Warung Bu Mil + 2 more in the app Saving again tells me you really want this - noted!
special message๐Ÿ“‚ Bali picks

A14 · cross-user cache hit1 DM

๐Ÿ‡ฎ๐Ÿ‡ฉ Bali ยท 7 spots found ๐Ÿ–๏ธ Padang Padang Beach ๐Ÿœ Warung Bu Mil + 5 more in the app Saving again tells me you really want this - noted!
special message๐Ÿ“‚ Bali picks

Re-shares skip the ack (detected during dedup, before extraction) and carry no Moment DM.

A16-A17 · Misses 1 DM

A16 · bookmark1 DM

This is definitely travel content but I couldn't pin down specific named places. Saved as a bookmark so you don't lose it.
special message๐Ÿ“‚ My collection

A17 · extraction failed1 DM

Hmm, couldn't find specific spots in that one. Try a reel where someone names the places they visit - those work best.
special message๐Ÿ”„ Try another

Part B · The copy catalog (current → now)

Because the voice stayed sentence case, most copy is unchanged. The real diffs are the new reel ack, the summary lead line, the reworded new-country moment, and Title-Case button labels dropped to sentence case.

B1 · Save acks & results (where the work is)

TriggerBeforeNow
Fresh reel ack (NEW)none โ€” result sent inline, no ackGot it - pulling the travel spots from this one. One sec ๐Ÿ“ธ
Carousel ackGot it - let me pull all the photos. One sec! ๐Ÿ“ธGot it - let me pull all the photos. One sec ๐Ÿ“ธ
Result DM leadheader first (Found them ๐Ÿ‘‡)summary sentence first, then header + places
Result body extrasFirst India spots โœจ / Gold mine. / 8 India spots total now.removed — the moment carries that
Insightseparate DM, +500msfolded into the Result summary line

B2 · Moment DM copy (one fires, by priority)

PriTriggerDM textCTA
1Milestone 1Your first spot is saved ๐ŸŒ This is where your collection begins.๐Ÿ“‚
1Milestone 55 spots and counting. A few more and I can start building you trip itineraries.๐Ÿ“‚
1Milestone 10Double digits! Open the app to see these on a map ๐Ÿ—บ๏ธ๐Ÿ“‚
1Milestone 2525 spots across {c} countries. Your itinerary builder has serious material to work with.๐Ÿ“‚
1Milestone 5050 spots. You're building a proper travel guide at this point.๐Ÿ“‚
1Milestone 100100 spots. Genuinely impressed. You might know more hidden gems than most travel bloggers.๐Ÿ“‚
2New country{flag} That's country #{N} in your collection. (was {flag} Country #{N}!)๐Ÿ—บ๏ธ Plan a trip
315+ saves, 0 tripsYou've got {n} spots across {c} countries but no trips yet. Ready to turn these into an itinerary?๐Ÿ—บ๏ธ Create a trip
3Inactive 7d+Hey, been a while! Your collection has {n} spots across {c} countries. Ready to add more?๐Ÿ“‚ My collection
โ€”none(no Moment DM)โ€”

Nudges keep the 1-per-24h cooldown (bot:nudge_sent:{userId}), so the nudge rarely wins the slot.

B3 · Button labels — Title Case → sentence case

BeforeNow
๐Ÿ“‚ My Collection๐Ÿ“‚ My collection
๐ŸŽฌ Send a Reel๐ŸŽฌ Send a reel
โœจ Get Startedโœจ Get started
๐Ÿ—บ๏ธ See Your Spots๐Ÿ—บ๏ธ See your spots
โœจ Create Accountโœจ Create account
โœจ Open in Appโœจ Open in app
๐Ÿ—บ๏ธ Create a Trip๐Ÿ—บ๏ธ Create a trip
๐Ÿ—บ๏ธ Build Full Trip๐Ÿ—บ๏ธ Build full trip
๐Ÿ—บ๏ธ Plan in App๐Ÿ—บ๏ธ Plan in app
โœจ Open Compass · ๐Ÿ”— Connect Instagramunchanged (proper nouns)

B4 · Everything else (unchanged): onboarding / OTP, the not-linked greeting, conversational greetings, quick-reply responses, guest caps, carousel failures, and the edge / failure messages keep their existing sentence-case copy — already terse and correctly capitalized, so the re-voice left them as-is.

Part C · What changed in code

FileChange
services/ai.service.tsextractActivitiesFast gained a summary field (schema + prompt RULE 6) — same Gemini call, $0. Slow path reuses the existing V8 insight.
lib/bot-messages.tsformatSmartProcessingComplete leads with a summary line and drops the milestone-flavored context lines; new pickMoment helper; new-country reworded; new reelWorking ack; labels to sentence case.
queues/content-consumer.tsthe +500ms insight DM is gone (insight → Result summary); the milestone / new-country / nudge fan collapses to one pickMoment send; the setTimeout chain is dropped.
routes/webhooks.routes.tsfires the instant reelWorking ack (once per batch, before the fast extraction); threads the fast summary into the result DM.
services/instagram.service.tsunchanged transport — still text-then-button. (Menu label sentence-cased.)
__tests__/dm-summary-and-moment.test.tsnew: summary leads the bubble, no moment flourishes in the body, pickMoment priority.

Verified: tsc --noEmit clean · eslint clean (incl. the no-AI-tell rule) · vitest run1171 passed, 1 skipped, including the 9 new assertions.