Skip to content

Changelog

· v0.5.0

A queue past the hourly limit

On a reel that takes off, DMs past Instagram's hourly limit now wait their turn instead of failing. Before this release, a DM over the limit was refused and never tried again, so that comment's one private reply never arrived. Each account now sends at most 12 DMs a minute, refusals that Instagram means as "not now" are tried again, and the comment poll reaches every post your rules cover.

Upgrading from 0.4.0

  • Run npm run update, or npm run deploy after pulling by hand. Migrations 010 (the send queue) and 011 (a hold on an account whose token stopped working) run before the new code goes live. (#139, #141)
  • The second cron trigger in wrangler.toml changes from every 5 minutes to every minute. The deploy registers it, so there's nothing to change in Cloudflare's dashboard. A copy that keeps the old */5 * * * * line still works, and sends its queue every 5 minutes. (#139)
  • Comments that no rule matches no longer appear under Last 20 sends on Home, because they're no longer written to the database. Use the Test page to see why a comment didn't match. Comments skipped for an exclusion, for being over 7 days old or for a paused account are still listed. (#139)

Added

  • The send queue. Each account makes at most 12 DM calls a minute, which keeps every hour under Instagram's limit of about 750, and a comment past that waits as Queued. A job every minute sends the queue oldest comment first, with the rule's text as it is then: turning a rule off or deleting it skips its queued DMs. A comment still waiting an hour before Instagram's 7-day limit is skipped as Expired. A backlog of 5,000 on one account takes about 7 hours. (#139)
  • Retries. A DM that Instagram turns down because the account is sending too fast, or because of a temporary error on its side, shows as Retrying and is tried again after 1, 5 and 20 minutes. The fourth refusal is final. A rate limit also holds the account's other DMs for 15 minutes. A DM refused because the token stopped working stays queued without using up a try, and goes out once you reconnect. (#141)
  • Send again, on a DM marked May have been delivered, for when you've checked that it didn't arrive. (#141)
  • A Logs page: every send, newest first, paginated and filterable by status, account, rule and date range, with Send again on DMs that may have been delivered. Thanks to @ranita872. (#64)
  • A Sending table on Home while DMs wait: queued and retrying counts for each account, the oldest comment's age, roughly how long the rest will take, and how many DMs went out in the last hour. The Accounts page says how many waiting DMs pausing an account will skip. (#141)

Changed

  • A DM whose answer from Instagram never arrived (a timeout, a server error, a dropped connection) is recorded as May have been delivered instead of failed. It still counts as failed in the totals, and the Worker still never sends it again by itself. (#141)
  • The comment poll now runs on every fifth minute of the minute job, and what it finds goes out in the same run, under the same limit. (#139)
  • A webhook delivery carrying many comments sends the first ones straight away and queues the rest, so it stays inside Cloudflare's 50 subrequests per invocation. (#139)

Fixed

  • The poll read only the first 3 posts your rules covered. A rule on a fourth post relied on the webhook alone, and with three rules on single posts, rules on all posts weren't polled at all. It now takes turns through every one, 3 posts per account each time: with 4 to 6 posts, each is read every 10 minutes, and with 7 to 9, every 15. (#140)

· v0.4.0

Send once, log out everywhere

Hardening from a review the same afternoon. A DM or public reply goes out once even when Instagram's answer gets lost, logging out ends every session, and webhooks that fail the signature check can no longer write to your database at will.

Upgrading from 0.3.0

  • Run npm run update, or npm run deploy after pulling by hand. Migration 009 adds the table that records extra public replies.
  • Deploying this version logs you out of the admin panel on every browser and device, once. Log in again. (#128)

Added

  • A rule can post its public reply when an earlier rule sent the DM, if you tick "Also post this public reply when another rule sends the DM". Up to two of these go out per comment, only after Instagram confirms the DM, and Home lists them under the send they belong to. Leave it off for any reply that says "Sent you a DM". (#129)
  • Comments on a boosted post's ad match the rules for the original post. Before, a rule for one post ignored the comments on its boost. (#123)
  • Home and npm run doctor remember when Instagram last reached the Worker, so after a quiet week they give the date instead of saying no notification has ever arrived. (#138)

Changed

  • Each DM and public reply gets one attempt. If Instagram's answer is lost to a timeout, a server error or a dropped connection, the send is recorded as failed with "may have been delivered" and isn't retried, because a retry could send it twice. (#124)
  • Comments on a paused account are skipped and logged as "account paused". Before, they waited, and resuming the account answered up to 72 hours of them at once. (#137)
  • npm run setup shows the admin link and verify token as soon as it saves them. If a run stops before the end, the next one replaces both and shows the new ones, since Cloudflare can't show a saved secret again. (#130)
  • npm run selftest needs ADMIN_PASSWORD, won't create a password on a deployed Worker unless you pass --allow-setup, and deletes its test account when it's done. (#133)
  • For contributors: pull requests say whether AI was used, and the issue-assignment check runs again when a description is edited (#122). The slowest integration tests get more time on a busy machine (#132).

Fixed

  • npm run setup and npm run doctor stopped at "not logged in" on Windows. (#127)
  • With two accounts connected and one needing to reconnect, the banner didn't say which. (#131)
  • Saving a rule for an account that isn't connected gives a form error instead of a 500 page. (#135)

Security

  • Logging out ends every session on every browser and device, and so does resetting the password. Before, a copied session cookie stayed valid for its full 30 days through both. (#128)
  • A login attempt takes its place in the limit before the password is checked, so attempts sent in parallel share the same five free tries. (#125)
  • Webhook POSTs that fail the signature check no longer write freely to D1. Unsigned ones write nothing, and a wrong signature is logged at most once a minute. Bodies over 256 KB are refused, and the "signing secret did not match" banner needs the three newest signed deliveries to fail. (#126)
  • The Connect callback shows Instagram's error text only when your own Connect comes back, so nobody can link to a page on your Worker that shows words they chose. (#134)
  • The project's GitHub Actions are pinned to commit SHAs, so a moved tag can't change what runs the secret scan or the docs deploy. (#136)

· v0.3.0

Exclusions, templates and npm run update

The first release with a test suite behind it. Every pull request now runs the real Worker against a local database with Instagram faked, and writing that suite turned up several of the fixes below. Rules gained exclusions, a contains mode, a reply-to-everyone mode, {username} and reply variations, and from here on npm run update brings each new version into your copy.

Upgrading from 0.2.1

  • You need Node.js 22.13 or later. On an older version npm install stops with "Unsupported engine". (#113)
  • Deploy with npm run deploy, not npx wrangler deploy. It now applies database migrations before the new code goes live, and this release brings five (004 to 008). Without them you'll see errors that mention "no such column". The first run also records the two migrations your database already has, which changes nothing. (#95)
  • Your copy doesn't have npm run update yet. Either fetch the script once and run it, or update by hand with git pull, npm install and npm run deploy.
  • Rules you don't edit keep working as they are. Saving a rule is stricter now (see Changed), so an old rule with a keyword like AI!! or a link to example.com has to be fixed the next time you save it.

Added

  • Exclusions. A rule skips a comment that also contains one of its exclusions, like "how much" or "not interested", and the next matching rule gets its turn. Home and the Test page show which exclusion stopped it. (#103)
  • A match setting on each rule: whole words, as before, or anywhere in the comment, so launch matches #launch2026. (#98)
  • "Reply to every comment on this post", for giveaways and waitlists: a rule with no keywords that answers everyone who comments on one post. It has to name a post, so it can't DM every commenter on the account. (#106)
  • {username} in a DM or public reply becomes the commenter's Instagram username. When Instagram doesn't send one, the message still goes out, without the placeholder and the comma or space beside it. (#100)
  • Up to five public replies per rule, one picked at random for each comment, so a busy post doesn't fill up with the same line. (#101)
  • Eight rule templates that fill in the new-rule form for you to edit: free guide, product link, price list, booking page, waitlist, event RSVP, menu and giveaway entry. (#114)
  • Home totals for today, the last 7 days, the last 30 days and all time: people reached, DMs sent, skipped and failed, and the same for each account when you have more than one. (#104)
  • On a new install, Home opens with a three-step "Get started" list. Saving, pausing, resuming or deleting now confirms itself in one line. (#116)
  • The admin panel can be installed on a phone's home screen, and every admin page works at 375px wide with 44px tap targets. (#111)
  • /health for uptime monitors. It answers 200 while the database responds and both scheduled jobs are on time, and 503 with the reason when they aren't. (#93)
  • npm run update pulls the latest version into your copy, applies new migrations, deploys and checks /health. It handles clones and forks, Deploy to Cloudflare copies (with --push) and ZIP downloads, and stops rather than touch edits you haven't committed. (#110)
  • This docs site, with search, and a landing page. (#105)
  • In Claude Code, say "set this up" and a bundled skill walks you through setup while you keep the logins and secrets. (#113)

Changed

  • Keywords are measured after accents and punctuation are stripped. Each needs at least 3 characters, or 2 if written in Chinese or Japanese, and at least one letter or digit: AI!! is now too short and 🔥🔥🔥 is refused. (#98, #109)
  • A rule can't be saved while its DM or a public reply still contains PASTE-YOUR-LINK-HERE or a sample address like example.com. That covers every rule, not only ones started from a template. (#114)
  • When the DM fails, the public reply is held back, since "Sent you a DM!" would be untrue. Home says why it wasn't posted. (#94)
  • The rule form asks for the name, keywords and message first. The match setting and exclusions fold away until a rule uses them. (#117)
  • The admin panel says "DM" throughout, gets its plurals right, keeps its menu on one row at 375px, and has easier-to-see inputs, placeholders and keyboard focus. (#116, #119)
  • A Home visit reads a bounded number of rows however long you've been running. All-time totals come from a nightly snapshot plus the sends since, so a visit over 2,010 old sends reads 47 rows instead of 8,040. That matters on D1's free plan, where every query fails for the rest of the day once 5 million rows have been read. (#121)
  • Keyword matching is about ten times faster with a few hundred keywords. (#96)
  • The README is shorter and no longer offers the Deploy to Cloudflare button; npm run setup is the supported way to deploy. Copies made with the button still update with npm run update -- --push. (#115)
  • For contributors: every pull request runs the Worker end to end in workerd against a fresh local database, with Instagram faked (#87). A pull request has to come from an issue its author is assigned to (#90), and coding agents get AGENTS.md (#113).

Fixed

  • A send that couldn't reach Instagram stayed "In progress" forever and blocked any retry. It's now recorded as failed, with the reason. (#94)
  • Requests to Meta time out after 10 seconds. A send the Worker never finished is marked failed by the daily job after 15 minutes, with a note that the DM may or may not have gone out. (#102)
  • A slow send no longer gets cut off by the 30 seconds Cloudflare allows after a webhook: Meta calls stop at 25 seconds and the result is written in the time left. (#112)
  • Delete on a rule deleted it at once, because the confirmation dialog never ran under the Content Security Policy. It now opens a confirmation page. The DM's character counter never ran either; the box now stops at 1,000 characters. (#94)
  • The Test page could name a different rule from the one a real comment triggers. (#94)
  • A comment containing a long number, such as an order number, could scramble the webhook's IDs and send the DM to a comment that doesn't exist. (#97)
  • One unreachable account stopped the nightly token refresh for every account after it. (#97)
  • A paused account's token wasn't refreshed, so an account paused for 60 days came back with a token that could no longer be renewed. (#102)
  • Connect reported a saved account as a failure when subscribing it to notifications hit a network error. (#97)
  • Letters like đ, ł, ø and ß are folded when matching, so dien matches điện and lodz matches łódź. (#92)
  • On a fresh install Home said the daily job "hasn't run in over 3 days". It now says when to expect the first run. (#104)
  • Unsigned POSTs from scanners no longer raise the "signing secret did not match" banner, and the "database tables are missing" page only appears when they are. (#102)
  • The 5-minute poll's limit of 10 comments per run counted comments the webhook had already answered. (#118)
  • A cancelled or refused Connect shows "Not connected" with a way back to Accounts, instead of a dead end. (#119, #120)
  • Wrong passwords sent at the same moment were counted as one. (#97)
  • Login waits longer than a minute are rounded up, not down. (#120)

Security

  • Connecting an Instagram account needs your admin login. Instagram's callback has to return to the same browser within 10 minutes, carrying a signed state. (#99)
  • Installing the panel on Android sends its manifest, which contains your admin URL, to Google's WebAPK service. Your password still protects the panel, and the security page explains it. (#111)

· v0.2.1

Keywords in any script

Two matching fixes, and the first pull requests merged from outside contributors. The troubleshooting guide had listed both bugs as known since 0.2.0.

Fixed

  • Keywords in non-Latin scripts match. Before, a keyword in Cyrillic, Arabic, Devanagari, Korean, Chinese, Japanese or Thai never matched any comment, because the word-boundary check only understood ASCII. Chinese, Japanese and Thai don't put spaces between words, so there is no word edge to check, and a keyword in those scripts also matches inside a longer word: ガイド matches ガイドライン. Thanks to dchaudhari7177. (#56)
  • Accents on Latin letters are ignored when matching, so the keyword preco matches "qual o preço" and preço matches "preco". Thanks to anisayakmitra-in. (#57)

Changed

  • Hono, the Worker's only runtime dependency, is updated from 4.13.2 to 4.13.5. (#15)
  • For contributors: TypeScript 7, newer Wrangler and Workers types, and newer GitHub Actions. (#10, #11, #12, #14, #16)

· v0.2.0

One-command setup

The release that made the repository public. Deploying went from fourteen commands and a hand-edited wrangler.toml to npm run setup, and the admin panel's secret handling was tightened now that anyone can read the code.

Upgrading from 0.1.0. wrangler.toml no longer has a database_id placeholder. If you replaced the placeholder with your own database's ID, git reports a conflict on that line when you pull: keep your line. After you deploy, open Home. It now lists any secret that's missing or malformed, with the command that fixes it.

Added

  • npm run setup runs the whole first deploy. It checks your Cloudflare login, creates the database and its tables, asks for the three values from Meta's dashboard, generates every other secret, deploys, and prints what to paste back into Meta. Secrets reach Cloudflare through stdin, never the command line. Running it again keeps your secrets; --rotate replaces them. (#17)
  • npm run doctor checks a deployment from the outside for the silent failures behind most "nothing happens" reports, such as a missing FACEBOOK_APP_SECRET or a Worker no notification has ever reached. It also names the two things it can't check: whether your Meta app is Live, and whether Hidden Words hid the comment. (#17)
  • npm run keys prints the four secrets you generate yourself, at the right lengths. (#9)
  • Home lists every secret that's missing, decodes to the wrong length, or is used for two jobs, with the command to fix each one. (#13)
  • A troubleshooting guide organised by symptom, and the setup steps in a guide of their own. (#18)
  • SECURITY.md, with the threat model and how to rotate each secret if it leaks, and the MIT license. (#9)

Changed

  • A fresh clone deploys with no file edits: Wrangler creates the D1 database from the name in wrangler.toml. (#17)
  • The README is a short front page, and the operator manual moved into docs/. Every roadmap item links to an issue that describes the approach. (#18, #55)
  • For contributors: CI runs the type check, unit tests, a dry-run deploy and a secret scan on every pull request, and the repository has issue templates and a contributing guide. (#9)

Security

  • Admin pages send Referrer-Policy: no-referrer. Before this, following a link from the admin panel to Meta or Cloudflare sent your full admin URL, secret included, to that site. (#13)
  • Login is rate limited: after five wrong passwords you wait a minute, and the wait doubles up to an hour. (#13)
  • The secret in the admin URL is compared in constant time, and every page gets a default-src 'none' Content Security Policy. Admin and Connect pages are sent with no-store, so a shared cache or the browser's back button can't bring them back after you log out. (#13)

· v0.1.0

First working Worker

The first version that answers real comments: one Cloudflare Worker and one D1 database on your own account. Someone comments a keyword, the Worker sends them one DM (Instagram's private reply), and it can post a public reply under the comment as well. Getting from the first deploy to live comments took eight pull requests, all on the same day.

Added

  • Rules: the keywords to watch for, the DM to send, an optional public reply, and either one post or all of them. Each comment is claimed in the database before anything is sent, so it gets one DM even when Instagram delivers it twice. (#1)
  • An admin panel behind a secret URL and a password. Home shows your accounts, the last 20 sends and whether Instagram is reaching the Worker. The Test page shows which rule a comment would trigger without sending anything. (#1)
  • A nightly job that refreshes each account's Instagram token before it expires. (#1)
  • Connect subscribes the account to comment notifications, and Home and the nightly job subscribe it again. Turning the field on in Meta's dashboard isn't enough for live comments to arrive. (#5)
  • The rule and Test forms list your recent posts and reels to pick from, instead of asking for a Graph media ID. (#6)
  • /privacy, /terms and /data-deletion pages, which Meta asks for before it lets you publish the app. Only a Live app receives real comments. (#8)
  • A poll every 5 minutes that picks up recent comments the webhook missed. (#8)
  • Setup notes for inviting every creator account as an Instagram Tester (#2), and for where each secret comes from and the per-account Webhook subscription toggle (#7).

Fixed

  • Connect builds its redirect address from the Worker's own address, so Instagram stops answering "Invalid redirect_uri" while PUBLIC_BASE_URL is still a placeholder. Before sending you to Instagram, Connect shows the exact addresses to list in Meta, with and without a trailing slash, and the App ID it's using. (#3, #4)
  • Webhooks signed with the Facebook app secret are accepted as well as ones signed with the Instagram app secret. Meta often signs live comments with the Facebook one, so it's stored as a second secret, FACEBOOK_APP_SECRET. A rejected signature is logged, so Home shows it instead of staying empty. (#8)
  • Instagram IDs too long for a JavaScript number keep every digit when the webhook is read, so the account lookup and the DM go to the right place. (#5)