Using it
Connect accounts, write keyword rules, test a rule without messaging anyone, and read the logs.
Everything here happens in your admin panel, at the admin URL npm run setup printed (https://ig-comment-dm.YOURNAME.workers.dev/a/ADMIN_URL_SECRET). The first visit asks you to create a password.
Connect an account
Accounts → Connect a new account. Instagram asks you to allow access; allow it. The Connect page also shows the exact OAuth redirect URIs and the Instagram App ID the Worker is sending, which helps when Meta rejects the login.
You can connect several Instagram accounts to one deployment. Each one has to be an Instagram Tester on your Meta app, with its tester invite accepted and its Webhook subscription turned on in Meta.
Connect uses whichever Instagram account that browser is logged into. If one profile connects and another doesn't, that's usually why: open a private window, log in to your admin page there, and click Connect from that window.
Connecting needs you to be logged in to the admin panel; the Connect page refuses anyone who isn't. Pause on the Accounts page stops DMs for an account without disconnecting it, and the nightly job keeps its token fresh while it's paused. Comments Instagram delivers while it's paused, and DMs still queued when you pause it, are logged as skipped, so resuming doesn't answer them late.
Write a rule
Rules → New rule.
The form asks for the essentials first: a name, the keywords, and the message. Everything else has a sensible default.
| Field | What to put |
|---|---|
| Rule name | For you only, e.g. Free guide. |
| Keywords | One per line, at least 3 characters each, counted after emoji and punctuation are dropped. AI would fire on "again" and "email", so the form rejects it. Chinese and Japanese need 2, so 指南 works. |
| Reply to every comment on this post | Instead of keywords, for giveaways and waitlists. Everyone who comments on that one post gets the DM, and the keyword fields disappear. It needs a specific post: on all posts it would message everyone who ever comments, so it's refused. |
| Exclusions and matching | Folded away, since most rules don't need it. It opens by itself on a rule that uses either setting. |
| ↳ How to look for keywords | Whole words only (the default): guide matches "Guide please!" but not "guides". Anywhere in the comment: also catches #launch2026 and "guides", but art would match "start". |
| ↳ Exclusions | Optional, one per line. A comment containing any of them is skipped by this rule even when a keyword matches: how much, already have it, not interested. |
| ↳ Also post this public reply when another rule sends the DM | Off by default. Tick it only if this rule's public reply doesn't mention a DM. See below. |
| Which posts | All posts and reels (which includes reels), or one post picked from the dropdown. A rule for one post also answers comments on its ad when you boost it. |
| DM | Up to 1,000 characters. Put everything they need in this one message. {username} puts in their Instagram name. |
| Public reply | Optional. Posted under their comment, and only once the DM has gone through. {username} works here too. Open Add variations to write it a few ways, and each comment gets one at random. |
Put everything in that one message. Instagram allows exactly one DM per comment, ever. You don't get a follow-up unless they reply to you first.
The post dropdown lists your recent posts and reels after you connect. A post's ID is a long number, not the share link instagram.com/reel/….
Start from a template
The Rules page lists eight starting points: a free guide, a product link, a price list, a booking page, a waitlist, an event RSVP, a menu and a giveaway entry. Picking one opens the new-rule form with the keywords, the DM and three public replies already written. Nothing is saved until you press Save rule, and you can change every field first.
Each template has PASTE-YOUR-LINK-HERE where your link goes. The form won't save while that's still in the DM or a reply, or while either points at a sample address such as example.com or calendly.com/example, so a follower is never sent a page that isn't yours.
The free guide answers guía and गाइड as well as guide, for followers who comment in Spanish, Portuguese or Hindi. Delete the ones your audience won't type. The giveaway entry replies to every comment, so it needs its post picked under Which posts.
The templates are plain text in src/templates.ts, so a better one is a pull request away.
How keywords match
- Matching ignores case, emoji and punctuation, on both the keyword and the comment:
guidematchesGuide!andguide 🙏. - Accents on Latin letters don't matter:
cafematchescafé,precomatchesPREÇO,dienmatchesđiệnandlodzmatchesłódź. Marks in other scripts are part of the word, so type a non-Latin keyword with the same marks the comment uses. - With Whole words only, the keyword has to stand on its own:
guidedoesn't matchguidesor#guide2026. Switch the rule to Anywhere in the comment for hashtag and brand campaigns. - Keywords work in any script, including Cyrillic, Arabic, Chinese and Japanese. Chinese, Japanese, Thai and other scripts written without spaces have no word edges to check, so there a keyword also matches inside a longer word:
ガイドmatchesガイドライン. - A keyword written entirely in Chinese or Japanese characters needs 2 characters rather than 3, so
指南can be saved but指can't. Thai, Lao, Khmer and Myanmar still need 3, because their vowel and tone marks count as characters, so two are often a single syllable. A keyword that mixes in Latin letters or digits, likea指, needs 3. A keyword that's only emoji or punctuation (🔥🔥🔥) is refused too, because it could never match anything.
Exclusions are found the same way as the rule's keywords, and they belong to that rule alone. When a comment trips one, matching carries on to the next rule, so another rule without that exclusion can still reply. If no rule replies because of an exclusion, Home lists the comment as Skipped: matched exclusion “how much”, and the five-minute check leaves it alone from then on. A comment that matches no rule at all isn't listed: writing a row for each would use up the database's free daily allowance on a busy account. The Test page shows what any comment would do.
When more than one rule could answer a comment, the first of these sends the DM: a keyword rule for that post, then a keyword rule for all posts, then a rule that replies to every comment on that post. So a specific keyword on a giveaway reel still gets its own DM. Within each group the oldest rule wins. Pause on the Rules page turns a rule off without deleting it.
Instagram allows one DM per comment, but any number of public replies. The other matching rules' DMs aren't sent, so by default their public replies aren't posted either: a reply like "Prices are in your DMs" would be false. Tick Also post this public reply when another rule sends the DM on a rule whose reply stands on its own, like "Prices start at $20!". Then, after the first rule's own public reply, up to 2 such rules post theirs too, in the same order, once Instagram has confirmed the DM. A rule stopped by one of its exclusions doesn't count as matching. Under each send, Home lists the other rules that matched and what happened to each one's public reply: posted, failed, over the limit of 2, only posted with the rule's own DM, or the rule has none. The templates leave the box unticked.
Replying to every comment
A rule with Reply to every comment on this post ticked has no keywords: every comment on its post matches. Each comment gets its own DM, so someone who comments three times gets it three times. Exclusions still apply, matched as whole words.
On an older post, the five-minute check below also answers the post's comments from the last 72 hours that it hasn't handled. Comments it already skipped stay skipped. On the Test page, pick the post under "Which posts" to see such a rule match.
The five-minute check
Every 5 minutes the Worker also reads recent comments itself, for the ones the webhook missed. It covers each post a rule is limited to, plus your 3 newest posts if any rule is on all posts, and reads 3 of them per account each time. When there are more than 3, it takes turns: with 4 to 6 posts each one is read every 10 minutes, with 7 to 9 every 15, and so on. From each post it reads the 25 newest comments from the last 72 hours. It queues up to 10 comments per check across all accounts, which the same minute starts sending under the hourly limit below, so a busy check leaves the rest for the next one.
Test a rule
Test → paste a sample comment. It shows which rule would send the DM, using the same rule choice as a real comment, the other matching rules and which of them would post a public reply, and the comment as it was matched, with case, emoji and punctuation stripped out. Paused rules aren't tested, and for a paused account it says so before naming the rule. Nothing is sent to anyone.
Then try it for real: comment on your post from a different Instagram account; a friend's phone works. Your own comments on your own posts are ignored on purpose, and the friend doesn't need to be a tester. The DM lands in their inbox, or in Message requests if they don't follow you.
While you test, turn off Instagram's Hidden Words filter on the creator account. Comments it hides never reach the Worker.
Read the Home page
| Panel | What it tells you |
|---|---|
| Get started | Shown until your first DM goes out: connect an account, write a rule, test it. Each step says Done once it's happened. |
| Totals | People reached, DMs sent, skipped and failed, for today, the last 7 and 30 days, and all time. Days start at midnight UTC, and the 7- and 30-day totals include today. People reached counts each person once, and only if a DM actually went out. With more than one account, each also gets its own. |
| Did Instagram reach us? | Whether Meta has POSTed to your webhook in the last 7 days. When it hasn't, this gives the date Meta last reached you. If it never has, the problem is on Meta's side, usually an app still in Development. Wrong secret means a request's signature matched neither app secret. It's logged at most once a minute, and only repeats with nothing valid between them raise the red banner, since a stranger can send one too. Requests with no signature aren't listed. |
| Sending | Shown while DMs wait, Instagram holds an account, or DMs went out in the last hour: how many are queued or retrying, the oldest one's age, roughly how long they'll take, and how many DMs went out in the last hour. Instagram allows about 750. |
| Last 20 sends | Every DM, skip and failure, with the reason and the exact error from Meta. Queued waits its turn under Instagram's hourly limit (below), Retrying was turned down for now and says when it's tried next, and May have been delivered has a Send again button for when you've checked it didn't arrive. View all logs opens the full history, filtered by status, account, rule or UTC date. |
| Red banners | An account needs reconnecting, the nightly job has stalled, or a secret is misconfigured. |
Check connections now on Home runs the nightly job on demand: it refreshes tokens and re-registers the webhook subscription. It's harmless to run as often as you like.
After you save, pause, resume or delete a rule, or pause an account, a green line at the top of the page confirms it.
Tokens last 60 days and the nightly job refreshes them. If an account shows Reconnect, click it: it takes about 20 seconds and keeps your rules and history.
When a post takes off
Instagram lets an account send about 750 private replies an hour. The Worker sends at most 12 a minute per account, which keeps every hour under that, and a comment past that is Queued rather than refused. Every minute, the Worker sends the oldest queued comments first, with the rule's text as it is then: edit a rule and its queued DMs go out with the new text, and turning a rule off or deleting it skips them. Pausing an account skips its queued DMs.
A backlog drains at about 700 an hour, so 5,000 waiting comments take about 7 hours. That's for one account: one Worker run a minute sends for every account, so two accounts with backlogs get about 7 a minute each, and three about 4. If Instagram turns a DM down for now, because the account is sending too fast or it has a temporary problem, the DM is tried again after 1, 5 and 20 minutes before it's given up on; a refusal that means it can never go out, like a comment already answered, isn't tried again. A DM Instagram never confirmed is never sent again by itself, because it may have arrived. Instagram refuses a private reply to a comment more than 7 days old, so a queued comment still waiting an hour before that is skipped as Expired.
The free database allows about 6,000 to 7,500 DMs a day (it counts rows written, and each DM writes 13 to 16 rows). Past that, it refuses everything until midnight UTC, comments included, so a reel that draws more matching comments than that in a day loses some. Cloudflare's paid Workers plan raises the limit to 50 million rows written a month.
Get told when it stops working
The red banners only help if someone opens the panel. Point any uptime monitor at:
https://ig-comment-dm.YOURNAME.workers.dev/health
It answers 200 while the database responds and both scheduled jobs are running, and 503 when either has stalled, so you hear about a dead nightly refresh before tokens start expiring. It needs no login and shows no usernames, IDs or secrets: only account counts and the time each job last ran. What a 503 means.
Put it on your phone
Open your admin URL on the phone, log in, then:
- Android, Chrome: menu → Install app (or Add to Home screen).
- iPhone, Safari: Share → Add to Home Screen.
It opens full screen, like an app. An iPhone keeps a home-screen app's cookies apart from Safari's, so you log in once more inside it. Installing on Android shares your admin URL with Google; Security explains why that's acceptable.
Logs
Live Worker logs. Leave this running, reproduce the problem, and watch:
npx wrangler tailNothing secret is ever logged (account IDs appear, tokens and keys never do), so this output is safe to paste into an issue.
What Instagram actually sent:
npx wrangler d1 execute ig-comment-dm --remote --command \
"SELECT received_at, status, object, error FROM webhook_events ORDER BY received_at DESC LIMIT 20;"Why a specific comment was skipped:
npx wrangler d1 execute ig-comment-dm --remote --command \
"SELECT comment_id, dm_status, reply_status, error, sent_at FROM sent ORDER BY sent_at DESC LIMIT 20;"A health check of the whole deployment:
npm run doctorIt checks your secrets, database tables, connected accounts, active rules, rejected webhooks and the scheduled jobs, and names the fix for anything it finds.