The actual playbook

How to replace ManyChat
in one weekend.

The exact system I built for my own IG. Keyword triggers. Follow gate. Auto-retrigger when they DM back. Every fire logged in a dashboard I own. No SaaS bill. No rate limits. No "upgrade to unlock" walls. Here is every step.

$0/mo
infrastructure
~4 hrs
first build
~3s
trigger to DM

01.What you are building

Someone comments a keyword on your IG post. Say SCORE or SYSTEMS.

02.The stack (two things)

No persistent server. No scaling config. You will never get a bill.

03.Step 1 — Meta app setup

  1. Go to developers.facebook.com → Create App → Business type.
  2. Add the Instagram product to the app.
  3. Connect your IG account (must be a Business or Creator account, linked to a Facebook Page).
  4. Generate a long-lived access token. Scopes you need: instagram_basic, instagram_manage_comments, instagram_manage_messages, pages_messaging.
  5. Under Webhooks → Instagram, subscribe to these fields: comments, messages, follows.
The landmine nobody warns you about. The follows webhook is unreliable. Meta drops some events silently. Your code must have a fallback or real followers will hit the gate and never get the link. More on that in step 6.

04.Step 2 — Supabase schema

Three tables. That's it.

-- Your automations (one row per keyword campaign)
create table keyword_automations (
  id uuid primary key default gen_random_uuid(),
  ig_user_id text,
  keyword text not null,
  match_type text default 'contains',  -- exact / starts_with / contains
  public_reply text,                      -- what to comment back publicly
  dm_message text not null,            -- use {link} as placeholder
  dm_link text,
  require_follow boolean default true,
  not_following_message text,
  active boolean default true,
  fire_count int default 0,
  last_fired_at timestamptz,
  created_at timestamptz default now()
);

-- Cache of your followers (populated by the follows webhook)
create table ig_followers (
  ig_user_id text not null,
  follower_ig_id text not null,
  follower_username text,
  followed_at timestamptz,
  unfollowed_at timestamptz,
  primary key (ig_user_id, follower_ig_id)
);

-- Every fire, every gate, every retrigger — all logged
create table automation_logs (
  id uuid primary key default gen_random_uuid(),
  automation_id uuid references keyword_automations(id),
  ig_user_id text,
  commenter_ig_id text,
  commenter_username text,
  trigger_type text,    -- comment / dm / follow_retrigger / dm_retrigger
  trigger_text text,
  matched_keyword text,
  was_following boolean,
  dm_sent boolean,
  error_message text,
  raw_payload jsonb,
  created_at timestamptz default now()
);

05.Step 3 — The webhook handler

One file at api/meta-webhook.ts. Three jobs: verify the webhook on GET, route incoming events on POST, send DMs through the Graph API.

export default async function handler(req, res) {
  // Meta verifies the URL with a GET challenge first
  if (req.method === 'GET') {
    const token = req.query['hub.verify_token']
    if (token === process.env.VERIFY_TOKEN) {
      return res.status(200).send(req.query['hub.challenge'])
    }
    return res.status(403).end()
  }

  // POST = real event
  for (const entry of req.body.entry || []) {
    for (const change of entry.changes || []) {
      if (change.field === 'comments') await handleComment(entry.id, change.value)
      if (change.field === 'follows')  await handleFollow(entry.id, change.value)
    }
    for (const msg of entry.messaging || []) {
      await handleMessage(entry.id, msg)
    }
  }
  res.status(200).json({ ok: true })
}

Comment handler pseudocode (the core logic):

async function handleComment(igUserId, v) {
  const auto = await findMatchingAutomation(v.text)
  if (!auto) return

  const following = await isFollower(igUserId, v.from.id)

  if (auto.public_reply) replyPublicly(v.id, auto.public_reply)

  if (auto.require_follow && !following) {
    await sendPrivateReply(v.id, auto.not_following_message)
    // IMPORTANT: log this as "gated" so we can retrigger later
    await log({ commenter_ig_id: v.from.id, was_following: false })
  } else {
    await sendPrivateReply(v.id, auto.dm_message.replace('{link}', auto.dm_link))
    await log({ commenter_ig_id: v.from.id, was_following: true, dm_sent: true })
  }
}
Two APIs, one system. IG Business accounts use graph.facebook.com. Newer Instagram-Login tokens (they start with IGAA) use graph.instagram.com. Your code needs to pick the right base URL per token or you'll get 400s that make no sense.

06.Step 4 — The follow gate retrigger (the secret sauce)

This is where most DIY builds fall apart. They ship the gate, someone follows, and the link never arrives.

Two recovery paths. Both required.

Path A — the follow webhook fires

When Meta sends you a follows event, look up any gated automation_logs row for that follower from the last 24 hours. If you find one, fire the real DM now and mark the log fulfilled.

Path B — they DM back instead

If Meta drops the follow event (it happens — trust me), the user often DMs back "done" or "ok" or "followed". When you receive a DM, check intent (either keywords or a quick LLM call) and do the same gated-log lookup. If there's a gated row, send the link.

Why this matters for real. I had a user follow and comment "SYSTEMS" — the follow webhook dropped. My ig_followers table didn't know. The gate fired. The user got stuck. Without Path B, that lead is gone. With it, they DM "done", the bot catches the intent, sends the link, and the gate resolves.

07.Step 5 — Sending the actual DM

The Graph API has two surface endpoints depending on how you're replying:

await fetch(`${BASE}/${igUserId}/messages?access_token=${token}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    recipient: { id: senderIgId },
    message: { text: dmText }
  })
})

08.Step 6 — Deploy

  1. Push to GitHub. Connect to Vercel. Deploy.
  2. Copy your Vercel URL (e.g. https://yoursite.vercel.app/api/meta-webhook).
  3. Paste it in Meta's webhook config. Set your VERIFY_TOKEN env var on Vercel to match.
  4. Hit "Verify and Save" in Meta. It fires a GET. You return the challenge. Green check.
  5. Create your first automation row in Supabase. Go comment the keyword on your own post. Watch it fire.

09.What this unlocks

Stuck halfway through? Meta webhook failing to verify? Want someone to just do it?

Book a 30-min call

10.Where most people quit

Three failure points — in order of how many times I've seen someone get stuck:

  1. Meta app review. To go live to real accounts you need App Review for the IG scopes. Takes 3–10 days. Most people give up here.
  2. The ID mismatch. The commenter's IG ID from a comment event does not always equal the sender's IG ID from a DM event, depending on your token type. You have to handle both.
  3. Silent follow events. 5–15% of follows never fire a webhook. If you don't build Path B (DM intent fallback), those leads are gone.

If you get past all three, you have a system that will run for years.

If you want the shortcut

I'll install this whole system for you.

Your account. Your Supabase. Your Vercel. You own everything. I get it live in a week. You never touch ManyChat again.

Book a 30-min call Or DM me on IG