Someone comments "guide" on your reel. They get a DM with your link, automatically, about two seconds later.
That is the entire product. Tools that do this start around $15/month and want your Instagram login on their servers. This is the same thing, running on your Cloudflare account, on the free tier, for $0 β and you can read all 4,400 lines of it in an afternoon.
| Hosted tools | Comment to DM | |
|---|---|---|
| Cost | $15β99 / month, forever | $0. Cloudflare's free tier covers 100,000 requests a day. |
| Your Instagram token | On their servers, alongside everyone else's | In your Cloudflare account, encrypted with AES-256-GCM using a key only you hold |
| Your audience's DMs | Pass through a third party | Go straight from your Worker to Meta |
| If they shut down | You lose the automation | Nothing changes. It is your Worker. |
| Rate limits | Whatever plan you bought | Meta's actual limits |
| Reading the code | No | 4,400 lines. One dependency. No build step. |
It uses Meta's official Graph API through Instagram Login. No scraping, no browser automation, and it never sees your Instagram password.
flowchart LR
A["π¬ Someone comments<br/>"guide""] --> B{{"Meta webhook<br/><i>HMAC signed</i>"}}
B --> C["β‘ Your Cloudflare Worker"]
C --> D{"Is it you?"}
D -->|yes| X["π« ignored"]
D -->|no| E{"Already handled?"}
E -->|yes| X
E -->|no| F{"Matches a<br/>keyword rule?"}
F -->|no| Y["π logged as skipped"]
F -->|yes| G["π¨ One private DM"]
G --> H["π Optional public reply"]
P["β± Every 5 minutes"] -.->|"catches what the<br/>webhook missed"| C
N["π Nightly 03:00 UTC"] -.->|"refreshes the<br/>60-day token"| C
style A fill:#FD1D1D,stroke:#833AB4,color:#fff
style C fill:#F38020,stroke:#833AB4,color:#fff
style G fill:#22aa66,stroke:#0a5,color:#fff
style X fill:#eee,stroke:#999,color:#333
style Y fill:#eee,stroke:#999,color:#333
Two independent delivery paths. The webhook is the fast one β a DM lands in about two seconds. The five-minute poll is the safety net, because Instagram quietly drops webhooks for collapsed and low-signal comments. A comment that the webhook missed still gets its DM, just a few minutes later.
Everything lives in one Worker and one SQLite database:
| Runtime | Cloudflare Workers β no servers, no containers, no cold-start cost |
| Database | Cloudflare D1 (SQLite) β five tables |
| Framework | Hono, server-rendered HTML |
| Dependencies | One. Web Crypto does the hashing, HMAC, and AES. |
| Client-side JS | None at all |
| Build step | None |
You need Node.js 20+, a free Cloudflare account, and an Instagram professional account (Creator or Business β switch in the app under Settings β Account type and tools).
git clone https://github.com/CharanMN7/ig-comment-dm.git
cd ig-comment-dm
npm install
npx wrangler login
npm run setupnpm run setup creates the database, generates every secret at the right size, deploys, and finishes by printing the exact strings to paste into Meta's dashboard:
[1/6] Checking your Cloudflare login
[2/6] Creating the database and deploying
[3/6] Creating the database tables
[4/6] Your three values from the Meta dashboard
[5/6] Generating the rest
[6/6] Deploying again so the secrets take effect
Done. Your copy is live.
It is safe to re-run, and it will not rotate secrets you already have.
Important
Before that, you need a Meta app β about 15 minutes of clicking, and the part that trips everyone up. docs/setup.md walks through every screen, including the four traps that cost people an evening each.
Stuck? Run npm run doctor. It checks your live deployment and tells you what is missing in plain words.
Why clone instead of a template repo?
Because a fresh clone needs zero file edits to deploy β Wrangler provisions the D1 database from the config, so there is no placeholder to fill in. Nothing about the code differs between deployments; the only per-user values are Cloudflare secrets, which never touch the repository.
A GitHub template would sever the upstream link. Meta renames dashboard screens, retires OAuth hosts, and changes signing behaviour often enough that git pull needs to keep working:
git remote add upstream https://github.com/CharanMN7/ig-comment-dm.git
git pull upstream master
npx wrangler deployYour secrets live in Cloudflare, so updating never touches them.
Open your admin URL (printed by npm run setup β bookmark it) and create a password on the first visit. Then Accounts β Connect a new account.
You can connect several Instagram accounts to one deployment. Each one needs to be an Instagram Tester on your Meta app.
Rules β New rule.
| Field | What to put |
|---|---|
| Name | For you only. Free guide. |
| Keywords | One per line. At least 3 characters β AI would fire on "again" and "email". |
| The DM | Under 1,000 characters. Put everything they need here. |
| Public reply | Optional. Shows under the comment. |
| Which posts | All posts and reels, or pick one from the dropdown. |
Matching ignores case, emoji, and punctuation, and matches whole words. A rule scoped to a specific post beats a rule that applies to everything.
Tip
Put everything in that one message. Instagram allows exactly one private reply per comment, ever. You do not get a follow-up unless they reply to you first.
Test β paste a sample comment. It shows which rule would fire. Nothing is sent.
Then comment from a different Instagram account β a friend's phone works. Your own comments on your own posts are ignored deliberately.
| Panel | What it tells you |
|---|---|
| Did Instagram reach us? | Whether Meta is POSTing at all. Empty means the problem is on Meta's side, not yours. Wrong secret means FACEBOOK_APP_SECRET is missing. |
| Last 20 sends | Every DM, skip, and failure, with the reason. |
| Red banners | An account needs reconnecting, the nightly job has stalled, or a secret is misconfigured. |
Instagram's rules, not ours. Each of these is enforced by Meta and cost someone an evening to discover.
| π¨ | One private message per comment, ever. A second attempt is blocked here, and Meta would reject it anyway. |
| π | Your own comments are ignored. Meta notifies you about them; acting would DM yourself and burn the one reply. |
| π | Comments older than 7 days cannot receive a private reply. They are logged as skipped. |
| π | Tokens expire after 60 days. A nightly job refreshes them. Miss it and the account needs a 20-second reconnect. |
| π§ | A Development-mode app delivers nothing. Meta only sends the dashboard Test button until you click Publish. This is the number one reason a correct setup does nothing. |
| π€ | Hidden Words eats comments before we see them. If Instagram's filter hides a comment, it never reaches the API at all. Turn the filter off while testing. |
| β³ | Missed comments arrive late, not never. The five-minute poll catches them. |
Core pipeline
- Comment β one official Instagram private reply, end to end
- Optional public reply under the comment
- Keyword rules with case, emoji, and punctuation normalisation, matched on word boundaries
- Post-scoped rules take priority over account-wide rules
- Reel and post picker, so nobody has to hunt for a Graph media id
- Self-comment guard β never DMs the account owner
- Exactly-once delivery, claimed atomically in the database before sending
- 7-day comment age guard, matching Meta's private-reply window
- Dry-run rule tester that sends nothing to real people
Reliability
- HMAC-verified webhooks, accepting either the Instagram or the Facebook app secret
- Five-minute reconciliation poll for comments the webhook never delivered
- Nightly token refresh with a 10-day lookahead and a reconnect warning
- Automatic
subscribed_appsregistration after Connect - Retry with jittered backoff on Meta 429s and 5xxs
- Large Instagram ids preserved exactly, instead of being mangled by JSON number parsing
- Inbound webhook log, including rejected signatures, so silence is diagnosable
Security and operations
- Instagram tokens encrypted at rest with AES-256-GCM
- Two-factor admin: a 128-bit secret path plus a password
- Signed,
HttpOnly,SameSite=Strictsessions with CSRF tokens on every form -
Referrer-Policy: no-referrerso the secret admin URL never leaks to a third party - Login rate limiting with a doubling lockout
- Constant-time comparison of the admin path secret
- Startup validation of all eight secrets, surfaced in the admin panel
-
npm run setupβ clone to deployed in one command -
npm run doctorβ diagnoses a live deployment - Multi-account support on a single deployment
- Hosted privacy, terms, and data-deletion pages for Meta review
Everything below is an open issue. Comment on one to claim it β good first issue items are self-contained and have the approach sketched out.
π― Automation
| Issue | Feature | Why it matters |
|---|---|---|
| #23 | Tracked links with click analytics | Know whether the DM actually worked, not just that it sent |
| #24 | Button templates in DMs | A tappable CTA converts far better than a bare URL |
| #25 | Opening DM + reveal on tap | Works around Meta's one-reply-per-comment limit legitimately |
| #26 | Follow gate | Deliver the link only after they follow |
| #27 | Follow-up DM after a delay | A second touch inside the 24-hour window |
| #29 | Reply to DMs and story replies | The same keyword rules, triggered by inbound messages |
| #30 | {username} and {link} placeholders |
Personalised copy without a rule per person |
| #31 | Public reply variations | Rotate replies so a comment section does not read like a bot |
| #32 | Match any comment | Fire on every comment on a post, no keyword needed |
| #33 | Bind a rule to the next reel you post | Set the automation up before the content exists |
| #34 | Ad-comment to organic-post mapping | Boosted posts report a different media id and silently miss |
| #35 | Per-account rate limiting | Stay under Meta's 750 private replies per hour |
| #36 | Send every matching rule's public reply | Today the second matching rule is silently dropped |
| #22 | Negative keywords | Skip "not interested", "how much is the guide" |
| #37 | Bulk rule import from CSV | Set up thirty posts without thirty forms |
| #38 | Rule templates | Working starting points per niche |
π€ Matching
| Issue | Feature | Why it matters |
|---|---|---|
| #19 | Non-Latin script matching | Word-boundary matching silently drops Cyrillic, Arabic, and CJK today |
| #20 | Latin diacritic folding | PREΓO should match a preco keyword |
| #21 | Whole-word vs. partial toggle | Hashtag campaigns need substring matching |
π Analytics
| Issue | Feature | Why it matters |
|---|---|---|
| #39 | Dashboard totals | Sent today, this week, this month, all time |
| #40 | Sends-over-time chart | Inline SVG, no JavaScript |
| #41 | Per-rule performance | Which keyword is actually earning its place |
| #42 | Top matched keywords | What your audience really types |
| #52 | Follower history | Instagram only keeps 30 days; daily snapshots keep more |
| #53 | Post insights | Views, reach, saves, and shares next to the sends |
| #43 | Shareable report page | A read-only link for a client, no login |
| #44 | Filterable, paginated send log | Twenty rows stops being enough quickly |
π Instagram coverage
| Issue | Feature | Why it matters |
|---|---|---|
| #28 | messages webhook field |
Prerequisite for DM triggers and the inbox |
| #54 | DM inbox | Read replies without leaving the panel |
| #48 | Typed Meta errors | "Token expired" beats "Graph API error 190" |
π Operations
| Issue | Feature | Why it matters |
|---|---|---|
| #45 | Tracked migration runner | Adding a migration should not mean editing two npm scripts |
| #46 | Health check endpoint | Uptime monitoring without a login |
| #47 | Operational event log | Cron failures are invisible until something breaks |
| #49 | Export rules and logs | Your data, portable |
| #50 | Dark mode | The panel is white at 1am |
| #51 | Installable on a phone | Approve and edit rules from where you actually post |
Tip
New here? These four are self-contained, need no Meta app to test, and have the approach written out in the issue:
#19 non-Latin keyword matching Β·
#30 {username} placeholders Β·
#50 dark mode Β·
#48 human-readable Meta errors
| docs/setup.md | Every Meta dashboard screen, and the traps in each |
| docs/troubleshooting.md | Symptom-first. Start here when nothing happens. |
| docs/architecture.md | Routes, pipeline, cron, crypto |
| CONTRIBUTING.md | Setup, house style, what belongs here |
| SECURITY.md | Threat model, secret inventory, rotation, private reporting |
Issues and pull requests are welcome. good first issue items are self-contained and describe the approach.
npm install
npm test # 30 unit tests, no network, no database
npm run typecheck
npm run dev # http://localhost:8787Read CONTRIBUTING.md first β particularly the constraint that keeps this project usable: one runtime dependency, no build step, no client-side JavaScript.
Found a security problem? Do not open an issue. See SECURITY.md.
MIT licensed Β· Built on Cloudflare Workers and Hono
Not affiliated with, endorsed by, or sponsored by Meta or Instagram.
Instagram is a trademark of Meta Platforms, Inc.