Skip to content
dawoodabdullah010Public

About

Comment a keyword on your Instagram post, get an automatic DM. Self-hosted, no database, no Meta App Review.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

dmgate

Someone comments a word on your Instagram post. They get a DM. That's it.

Free, self-hosted, and it does not need Meta App Review.

comment "sendit"  →  DM:           "here you go: yoursite.com"
                  →  public reply: "sent it over"

This is the one feature people pay ManyChat $40/month for. About 300 lines, no database.

👉 Don't set this up by hand. Hand the whole repo to your AI assistant.

Clone it, open it in Claude Code (or paste it into Claude / Cursor / whatever you use), and tell it to read AGENTS.md and do everything.

That file is written for the assistant, not for you. It knows the four places Meta fails with no error message, what each one actually means, and the order to do things in. Setting this up by reading docs yourself is the slow path.


Recommended: let Claude do it

Deploying is the easy part. Roughly 90% of the work is Meta's dashboard, and nothing automates that — it's clicking through screens where four separate steps fail with no useful error message.

So use an assistant that can read AGENTS.md, walk you through the Meta screens one at a time, and do the deploy for you at the right moment.

Clone the repo, open it in Claude Code, and send:

Set up dmgate for me.

Read AGENTS.md in this repo first and follow it exactly — it has rules about
how to walk me through this, and I want you to stick to them:

- give me ONE step at a time, then wait for me
- if I say a screen doesn't look like you described, ask me for a screenshot
  instead of guessing
- no jargon. explain anything technical in plain English before you use the word
- never ask me to paste my access token into the chat
- run the verification command after each stage instead of assuming it worked

I want people who comment a keyword on my Instagram post to automatically get
a DM. Ask me for the keyword and the message when you need them.

Start with step 0.

That file exists because the first setup went badly in exactly those four ways. It knows where Meta fails silently, what each cryptic error means, and the order to do things in — including why deploying early is a waste of time.

Using a browser-driving assistant (Claude in Chrome and similar)? The appendix at the end of AGENTS.md has three ready-to-paste prompts that drive Meta's dashboard directly — creating the app, fixing the Insufficient Developer Role error, and publishing.

No assistant?

SETUP.md is the same guide written for a human to read. It's longer and slower, but it gets you there.


What you need

Instagram a Professional account (Business or Creator). Personal accounts are blocked from the API.
Facebook an account, to create the Meta app. Instagram's API runs through Facebook's developer system.
Hosting Vercel free tier
Scheduling GitHub Actions free tier
Money none
Time ~30 minutes, most of it clicking through Meta's dashboard

You do NOT need: App Review, business verification, a Facebook Page, a database, or a credit card.


The setup, in eight steps

  1. Instagram → switch to a Professional account, set it Public
  2. Register as a Meta developer at developers.facebook.com/apps (separate from having a Facebook account — this is why "Create App" seems not to exist)
  3. Create an app, type Business. On the use case screen, Instagram is not among the six featured options — filter to Business messaging
  4. Add the Instagram product → "API setup with Instagram login", not the Facebook one
  5. Add the three permissions
  6. Add your account as an Instagram Tester, then accept the invite at instagram.com/accounts/manage_access/ → Tester Invites (this is where Insufficient Developer Role comes from)
  7. Generate an access token
  8. Deploy, then publish the app (Development → Live). A form, not a review.

Steps 6 and 8 are where everyone gets stuck. Both are covered in detail in SETUP.md.


Keywords

Set with KEYWORD. Matching is whole-word and case-insensitive, after stripping mentions, emoji and punctuation.

someone comments fires? why
sendit ✅
SENDIT / SeNdIt ✅ case-insensitive
sendit!!! ✅ punctuation stripped
sendit 🔥🔥 ✅ emoji stripped
@you sendit ✅ mentions stripped first
yo send me sendit please ✅ matches inside a sentence
senditover ❌ whole-word only
send ❌ not the keyword

Pick a word nobody types by accident. link, info, guide, resume and send all get typed in normal conversation — and that person then gets a DM they never asked for. Unsolicited DMs are the thing that actually gets accounts actioned. sendit, gimme, or your word plus a digit can't happen by chance.

Several keywords, different messages

Set KEYWORDS to JSON instead, and each word gets its own reply:

KEYWORDS='{"sendit":"here you go: yoursite.com","lens":"repo: github.com/you/lens|grab it: github.com/you/lens"}'

Variants still work inside each one, separated by |. KEYWORDS overrides KEYWORD and DM_TEXT when set.

  • keys are normalised, so "SendIt!" and "sendit" are the same rule
  • if two could match, the longer keyword wins — senditnow beats sendit
  • a keyword with an empty message is ignored rather than falling back to a default
  • malformed JSON falls back to the single-keyword pair instead of crashing the run

Messages

Both take multiple variants separated by |. One is picked per commenter, stably — the same comment always gets the same variant, so a retry can't post a second different reply.

DM_TEXT="here you go: yoursite.com|sent! here it is: yoursite.com|all yours: yoursite.com"
PUBLIC_REPLIES="sent it over|check dms|just dm'd you|check your inbox|sent it"

Why variants matter: Instagram treats repeated identical comments as spam. Post the same public reply 200 times under one reel and it starts hiding them, or you get "action blocked."

The DM is private — nobody sees anyone else's. The public reply sits under their comment where everyone sees it, and doubles as proof the thing works.

Message limit is 1000 bytes, not characters. Emoji cost 4 each. Oversized variants are rejected at startup rather than failing on every send.


Timings and limits

Every default here exists to stop it looking like a machine. Meta flags behaviour, not just rule-breaking.

setting default what it does
GAP_MIN_MS / GAP_MAX_MS 2500 / 9000 random gap between sends. A fixed interval is a machine signature.
MIN_COMMENT_AGE_SECONDS 45 won't answer a comment younger than this. Replying in 2 seconds every time is not human.
QUIET_HOURS 1-7 silent overnight, in QUIET_TZ. Nobody answers DMs at a steady clip at 4am. off to disable.
QUIET_TZ UTC set this. IANA zone, e.g. America/New_York. Leave it at UTC and the quiet window lands on the wrong hours — see below.
MAX_SENDS_PER_RUN 12 in practice the 45s time budget binds first, at ~6
POSTS_TO_SCAN 2 how many recent posts to watch
MAX_COMMENT_PAGES 8 pagination depth — 8 × 50 = 400 comments per post per run

Throughput: roughly 6 sends per run. At a 5-minute schedule that's ~70/hour; at 1 minute, ~360/hour. Meta's ceiling is 750/hour, so there's headroom either way.

A 3,000-comment reel over 48 hours works out at ~62 comments arriving per hour — comfortably slower than it processes, so no backlog ever forms. Only a sharp spike (say 500 in the first hour) creates a short queue, and even then everyone is served well inside Meta's 7-day reply window.

Also built in:

  • one DM per person per run — someone impatiently commenting three times gets one reply, not three
  • stops the whole run on a rate-limit or permission error rather than hammering the same failure
  • skips comments older than 7 days — Meta refuses those anyway

Configuration reference

variable default
IG_ACCESS_TOKEN — required. From the Meta dashboard. Expires every 60 days.
KEYWORD sendit the word to watch for (single-keyword mode)
KEYWORDS — JSON map of keyword → message. Overrides KEYWORD/DM_TEXT.
DM_TEXT — the DM. Variants separated by |.
PUBLIC_REPLIES 6 built in replies under the comment, separated by |
CRON_SECRET — any random string. Stops strangers triggering your automation.
QUIET_HOURS 1-7 hours to stay silent, read in QUIET_TZ. off disables.
QUIET_TZ UTC IANA zone the quiet hours are measured in. Set it to yours.
MIN_COMMENT_AGE_SECONDS 45
GAP_MIN_MS / GAP_MAX_MS 2500 / 9000
MAX_SENDS_PER_RUN 12
POSTS_TO_SCAN 2
MAX_COMMENT_PAGES 8
DRY_RUN false true matches and logs without ever sending
NEXT_PUBLIC_OPERATOR_EMAIL — contact shown on your privacy/terms pages

Endpoints

route
/api/run does the work. Needs the x-cron-secret header.
/api/run?dry=1 dry run — shows what it would send, sends nothing
/api/health is it alive? Catches an expired token and Development mode.
/privacy /terms /data-deletion the three legal pages Meta requires before you can publish

Those legal pages ship as routes on purpose: deploying satisfies Meta's publish requirements, so you never have to go generate a privacy policy somewhere and host it.

Always dry-run first. Meta allows exactly one private reply per comment, ever. A typo can't be taken back.


The schedule

The app does nothing on its own — something has to call /api/run. A GitHub Action is included at .github/workflows/run.yml.

Add two secrets under repo Settings → Secrets and variables → Actions:

RUN_URL       https://your-app.vercel.app/api/run
CRON_SECRET   the same value you set in Vercel

Then hit Run workflow in the Actions tab to start it.

Why it isn't a five-minute cron

cron: '*/5' looks like it runs every five minutes. On a quiet repo it does not. Measured on this one, schedule events fired at 15:34, 16:29, 17:28, 18:25, 19:32 — roughly hourly. GitHub throttles scheduled workflows and treats the expression as a request. A commenter would have waited up to an hour for their DM.

GitHub controls when a run starts. It does not control how long a run lasts. So the workflow starts once and then polls every 60 seconds for 5½ hours inside that single run. The concurrency block keeps one run active and one queued, so the moment the active run ends the queued one takes over and the throttling stops mattering.

This needs a public repo. Public repos get unlimited Actions minutes. On a private repo the same workflow would eat the monthly free allowance in about a day — use a hosted cron service like cron-job.org hitting /api/run every minute instead.

Vercel's own cron won't do it. The Hobby plan is capped at one run per day; */5 is rejected at deploy time with Hobby accounts are limited to daily cron jobs. Frequent crons need Pro.

If you're not posting right now, don't leave it on. Cancel the run from the Actions tab when the reel goes cold — otherwise it polls all night and finds nothing.


Things that cost real time to find out

You cannot cold-DM a commenter. Having their user ID from the comment doesn't entitle you to message them. Commenting does not open a messaging window — nor does liking, following, or viewing a story. The only route is a private reply addressed to the comment_id. One per comment, within 7 days.

That private reply doesn't open a window either. They have to act on it before you can send anything else. This is why creators put a button in the first message — a tap opens the 24-hour window. It's the mechanism, not decoration.

Delivery depends on whether they follow you. Followers get it in their Inbox. Non-followers get it in Requests, where most people never look. That's why captions say "must be following" — a platform constraint, not a growth hack.

The permission is manage_comments, not manage_messages. Counterintuitive, and the thing people get wrong.

In Development mode the comments API returns {"data":[]} with HTTP 200. No error. No warning. Indistinguishable from a post with no comments — even your own comments are invisible. Publish the app and they appear. This is the single most expensive thing to discover on your own, and it's documented nowhere else.

Instagram only returns ~25 comments per page. Without pagination a viral post silently serves the first 25 people and ignores everyone else.

The public reply is load-bearing. It's how this remembers who it already answered — there's no database. Skip it and the same comment gets retried forever, draining the time budget and eventually starving new commenters. This was a real bug here: throttling replies to 1-in-3 quietly broke dedupe.

The token expires after 60 days. Nothing warns you. It just stops answering people. /api/health reports it.


Staying out of trouble

The mechanism is sanctioned — Meta's official API, their documented endpoint, permissions you granted yourself. What gets people actioned is behaviour.

Handled for you: randomised gaps, no instant replies, quiet hours, rotating variants, one DM per person, hard stop on rate-limit errors, caps well under Meta's ceiling.

Set QUIET_TZ or quiet hours will fire at the wrong time. They used to be UTC-only, so the window landed on whatever hours UTC said. Several zones east of UTC turn the default 1-7 into the middle of your morning — the exact window where a post made overnight collects its comments. Nothing looks broken: the endpoint reports ok: true throughout, because staying quiet is a success. If replies stop while everything looks healthy, call /api/run by hand and read skipped.

If your audience is spread across many countries, turn quiet hours off. There's no hour that's dead in the US, the UK and Europe at once, so any window you pick silences somebody's peak.

Up to you:

  • pick a keyword nobody types by accident — this is the big one
  • send what you promised, nothing else. No follow-up drip sequences.
  • don't raise the caps because you can

Development

npm install
npm test          # matching logic
npm run verify    # typecheck + tests
npm run dev

scripts/reply.py is a standalone version — no hosting, no deploy — for testing the whole flow from your laptop before committing to any of this.


Deploying without a terminal

Only if you have no assistant and don't want to touch a command line. Get your Meta token first — this asks for it up front — then follow SETUP.md for everything else.

Deploy with Vercel

It only saves you the deploy, which is the easy part. It does nothing for the Meta dashboard, which is where the actual work is.


Licence

MIT. See LICENSE.

Public posts only, on an account you own. Uses Meta's official API with permissions you grant yourself. Doesn't scrape, doesn't log in as you, doesn't touch accounts you don't control.

About

Comment a keyword on your Instagram post, get an automatic DM. Self-hosted, no database, no Meta App Review.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages