Blog
Home

Building an Instagram reply agent: the code was the easy part

3 October 2026 · 15 views

Answering every comment on Instagram Reels takes more time than it should. So I built ReplyAgent: every five minutes it reads the new comments on my recent Reels, has Claude Haiku draft a reply in my tone, and puts the drafts on an approval page. Nothing is posted until I approve it.

It started with a session with Claude to write a Business Requirements Document. That took about an hour, and it covers more than I expected when I started:

  • Objectives with targets, such as a draft within 10 minutes of the comment, 80% of drafts approved without edits, and under 5 euros a month in total.
  • 23 functional requirements, prioritised: twelve Musts for the first version, the rest for later phases (regenerating a draft with a hint, email notifications, other platforms like YouTube and Google reviews).
  • 14 non-functional requirements: secrets only in Key Vault, only the comment text and caption sent to the model, comment text treated as data and never as instructions, no reply ever posted twice.
  • A cost estimate per item, a risk table with a mitigation for each risk, and a roadmap with go or no-go gates between the phases.
  • Even a look at the market: which products already do this, at what price, and whether a niche is left.

It paid off. With the requirements numbered, every pull request could point to the ones it implemented, and Claude Code had a clear definition of done.

After that, the code went smoothly. Nine pull requests, 128 tests, and Claude Code doing most of the typing. Then I tried to connect my Instagram account to the API and create one extra account to test with, and that took longer than everything before it.

This post is mostly about that second part. If you're about to build anything on the Instagram API, it might save you an evening.

What it is

A quick overview, because the setup steps only make sense with the architecture in mind:

  • An Azure Function (Flex Consumption, .NET) with a timer: it reads the Reels of the last few days, picks up new top-level comments, and stores them in Table Storage.
  • Claude Haiku 4.5 drafts each reply with structured output: either a reply, or a skip with a reason (spam, emoji only, abuse). A first draft costs about 546 input and 42 output tokens, so a fraction of a cent.
  • An approval page served by the same Function, behind a Cloudflare Worker on replies.koorevaar.com with Cloudflare Access (an email one-time PIN). The Function only accepts requests that carry both the Worker's origin secret and a valid Access token, so the azurewebsites.net address itself just says Forbidden.
  • Key Vault for the Instagram token and the Claude API key, and a weekly function that refreshes the 60-day Instagram token.

The plan was simple: turn my existing Instagram account into a Creator account, connect it to the API, and create one extra account to write test comments.

Step 1: one extra account

My own account was the easy part. Switching it to a Creator account is a setting in the Instagram app, and it's what the API requires. No Facebook Page is needed with "Instagram API with Instagram Login".

But ReplyAgent ignores the account's own comments, so I needed a second account to write test comments. Creating it should take two minutes. Here's what happened instead:

  1. Every username was "not available". Including random strings nobody would ever pick.
  2. A name got a green check, which turned into a red cross a moment later. I tried it anyway, and it was accepted.
  3. On my desktop: "Your submission is being reviewed." On my phone, at the same moment, a new account could start right away.
  4. The desktop account was disabled shortly after. The appeal screen said: "You cannot request another review of this decision."

To be fair, part of this was self-inflicted. Before signing up, I had Claude check a dozen candidate usernames with a headless browser, from the same internet connection. Together with my own attempts, that is exactly the burst of activity an anti-bot system is built to stop. What bothers me is not that it stopped me, but how it said so. A rate-limited check shows the same "not available" as a name that's really taken, so you keep trying, which makes it worse.

The same thing happened on the next login. On the desktop, my correct password kept giving "incorrect username or password". After a password reset it worked immediately. My guess is it was another block on a flagged network, with the error message of a typo.

What finally worked: create the account in the Instagram app on my phone, and simply take the name the app proposes instead of trying my own. For an account that only exists to write test comments, the name doesn't matter, and it can still be changed later in the settings.

One smaller thing from this round: the mobile website can't post Reels. Its upload dialog only shows photos. A photo post comes back from the API as FEED, not REELS, so ReplyAgent skips it. For the test Reel, the app it is.

I've seen this pattern before, setting up Google Family Link: a process that should take minutes, made opaque by automated safeguards.

Step 2: the Meta developer app

The Instagram API needs an app on developers.facebook.com. Some notes, in the order I hit them:

  • The developer account is your Facebook account. The Instagram account is separate, and you connect it later by logging in to Instagram in a popup. To create the app, I still had to link my Creator account to my Facebook account in the Accounts Center, which first failed with "This page isn't available at the moment" and then worked from a desktop browser.
  • "No businesses available." App creation asks for a business portfolio. You don't need one: there's an "I don't want to connect a business portfolio yet" option.
  • Tester invite. In the app, under App roles, you add the Instagram account as an Instagram Tester. The account then has to accept, on instagram.com: Settings, Apps and websites, Tester invites.
  • "Allow access to messages" comes with a warning that the app may not work properly without it. ReplyAgent only reads comments and posts replies, so I left it off. It works fine.

Then I generated a token, checked it (GET /me returned the username), wrote a comment from the second account, and started the Function locally.

Source Instagram: 0 items found, 0 new.

Step 3: my battles and how I solved them

The comments call returned this:

{ "data": [], "paging": { "cursors": { "before": "QVFI...", "after": "QVFI..." } } }

while the same Reel reported comments_count: 1. Not an error, just an empty list. Even a comment from the Creator account itself, which is an app tester, came back empty.

The cursors were the first hint: there's a cursor around something, so the comment exists, it's just filtered out before it reaches you. It turned out to be two settings, and I needed both:

  1. The permissions have to be added to the app itself, under Use cases, Customize, Permissions. I had accepted "Access and manage comments" in the token's login screen, and that's not enough. Without the permission in the app, Meta doesn't return an error, just empty data. After adding it, generate a new token: an existing token doesn't pick up new permissions.
  2. The app has to be in Live mode. Even with the permissions, development mode still returned nothing. Live mode needs a privacy policy URL and a category, nothing more. With standard access, no App Review is needed.

After both: 1 items found, 1 new, a draft on the approval page, and after approving it, a reply under the comment on Instagram.

I also tested what I expected to be the next wall: whether comments from accounts without a role in the app come through. My followers will never be app testers. With Live mode and standard access, they do. That would have been the difference between "switch the token" and "submit for App Review".

Step 4: what the real API taught the code

The code had 128 passing tests, written against the API shapes in Meta's documentation. Testing against the real API still found three things:

  • Nested replies only have an id. I wanted to skip comments I had already answered, and asked for replies{username}. The usernames weren't there. What is there: the account's own replies also appear as separate items in the same comments list, with its username. So a comment counts as answered when one of its reply ids belongs to one of the account's own items. No extra API calls.
  • Other people's usernames are missing. The commenter's username simply wasn't in the response. Harmless for the logic, since the agent only needs to recognise its own account, but the approval page showed an empty @ · until I fixed it.
  • Language. The prompt says to reply in the language of the comment. Two Dutch comments got one Dutch reply and one Portuguese reply. The agent now has to name the comment's language in its structured output before writing the reply, and there's a setting to always reply in one fixed language.

None of these show up in a unit test that was written from the documentation. That's what testing against the real API is for.

Step 5: Azure

The deployment itself is two GitHub Actions workflows: Infra (Bicep) and Deploy (the code). A few things that cost time:

  • Microsoft.Insights stayed on "Registering" for a long while on a subscription that had never used it. The Infra workflow needs it, so it waits.
  • Bash in a Windows terminal. The README's commands were written for Bash: a for ... do ... done loop, \ line continuations, <placeholders> that PowerShell reads as redirection. It now has a PowerShell version of each, with tokens entered through Read-Host so they don't end up in the PowerShell history.
  • Forbidden after logging in. Cloudflare Access let me in, and the Function said Forbidden. That's by design: it gives the same answer for every failed check, so a caller can't tell which one failed. The log says why: Rejected Access token for an email outside the allow list. I had separated the two allowed email addresses with a semicolon. The code splits on commas.
  • AppTraces doesn't exist when you query from the Application Insights Logs page. There the table is called traces, with timestamp and message. AppTraces is the name in the Log Analytics workspace.

That last generic Forbidden is a good example of the theme of this post, but from the other side. A security check that doesn't explain itself is the right call, as long as the person who owns the system can find the real reason in a log. Instagram's sign-up is the same idea without that second half.

Lessons learned

  • Expect the platform setup to take longer than the code. Writing the agent was mostly specifications and tests, things I control. The accounts, the app and the permissions run on someone else's anti-abuse system that you can't see into.
  • Create accounts in the phone app, take the name it proposes, and don't automate anything against the sign-up. If it starts saying "not available" to everything, stop for the day.
  • If the comments come back empty while comments_count says otherwise: add the permissions in the app (Use cases, Customize, Permissions), generate a new token, and switch the app to Live.
  • Test against the real API before you trust the shapes. Three of my assumptions were wrong in ways no documentation-based test would catch.
  • Write the steps down as you go. Every gotcha above is now a step in ReplyAgent's README, so setting it up for another account is a checklist instead of another evening like this one.

Co-authored with Claude.

Related articles

Comments

← All articlesView as Markdown