Build Soccial AI
KINDAreplaces $139/mosaves $1,668/yrback to the verdict
A single-account Instagram direct-message responder whose hard problems are a webhook that stays correct under signature checks and retries, a messaging window and token lifecycle that fail silently when you get them wrong, and a handover that genuinely stops the bot.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the webhook receiver and the worker.
Get it nodejs.org or your version manager. open ↗
Verify
node --version prints v22 or higher - accountfree
Why Nothing in this build works without them, and in development mode the app serves only accounts you have added, which is the correct scope here.
Get it Create the developer app, connect the professional account, subscribe to the messages webhook field and generate a long-lived token.
Verify
The verification handshake succeeds and a test message arrives at your endpoint - have readya few units of currency a month, or free with hardware you already run
Why Messages arrive at three in the morning and a laptop that sleeps loses them, which is the single most common reason this build looks broken.
Get it A tunnel from a small always-on machine, or a cheap virtual server with a certificate.
Verify
The endpoint answers the verification handshake from the internet while your laptop is closed - API keypay per reply; a personal account is small change
Why One call per reply, and the only recurring cost besides the endpoint.
Get it Create a key with your preferred provider and set a spend limit in their console.
Verify
A stored conversation produces a reply in your persona voice - decidefree
Why Every price, date and availability the bot is allowed to mention lives here, and deciding that after launch means deciding it from whatever it already said.
Get it Write the voice, the facts, the refusals and the handover triggers into one file before the first reply goes out.
Verify
A question outside the file produces a handover in testing
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- conversations(igsid, first_seen_at, last_inbound_at, last_outbound_at, disclosed_at, paused_at, human_requested_at) · the window is computed from last_inbound_at rather than stored - messages(id, mid, igsid, direction, text, at, is_echo, raw_json) · mid carries a unique index, which is the entire deduplication story - deliveries(id, mid_in, mid_out, status, error_code, attempts, next_attempt_at, at) · a send that failed is a row, not a log line - tokens(id, kind, value_encrypted, obtained_at, expires_at, last_refresh_at, last_refresh_error) · the expiry is the thing you will forget - handovers(id, igsid, reason, at, resolved_at) · why the bot stopped, so you can see whether it stops for the right reasons - persona_versions(id, text, active_from) · every stored reply references the version that produced it - The one thing this schema refuses to hold is a contact list. There is no table of people to message, because this application never starts a conversation.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
IG_ACCESS_TOKENsecret | required | IGAA...redacted | The long-lived user token. Stored with its expiry and refreshed on a schedule. |
IG_ACCOUNT_ID | required | 17841400000000000 | Your own account identifier, used to recognise and ignore your own messages. |
META_APP_SECRETsecret | required | redacted | Used to verify the request signature over the raw body bytes. |
META_VERIFY_TOKENsecret | required | a-string-you-choose | Echoed back during the subscription verification handshake. |
MODEL_API_KEYsecret | required | sk-...redacted | One call per reply. Give it a provider-side spend limit too. |
DISCLOSURE_TEXT | required | You are chatting with an automated assistant. | Refuses to start if empty. Check the current platform policy for what it must say. |
REDISCLOSE_AFTER_HOURS | required | 24 | Gap after which the disclosure is repeated in an existing thread. |
REPLY_MIN_INTERVAL_SECONDS | required | 120 | Per-person rate limit, enforced in the database so a restart does not reset it. |
The build, in order
A webhook that survives verification and retries
Deliveries are authenticated, acknowledged immediately and processed exactly once.
A parsed and re-serialised body is a different byte string.
Files
src/webhook.tstests/signature.test.tsThe platform retries a slow endpoint and you pay for that in duplicate replies.
Files
src/webhook.tssrc/queue.tsDeduplication belongs in the database, not in an if statement.
Files
src/db/schema.sqltests/dedupe.test.ts
done when · tick each as it passeswatch out- Letting a body parser run before the signature check means you are computing the digest over a re-serialised object and it will never match the one on the request, and doing the model call before returning 200 means the platform times out, retries the same delivery, and your account sends the same reply three times. Capture the raw bytes for the digest, acknowledge first and work afterwards, and put a unique index on the message id so a retry collapses into a no-op at the database level.
The messaging window, and saying that you are a bot
Sends outside the allowed window are blocked before the call, and the disclosure is part of the flow rather than a setting.
And check the current published duration before hard-coding one.
Files
src/window.tstests/window.test.tsRecorded on the conversation so re-disclosure can be timed.
Files
src/disclosure.tstests/disclosure.test.tsThe one setting that is not a setting.
Files
src/config.ts
done when · tick each as it passeswatch out- The standard messaging window runs from the person's last message and a follow-up you schedule for the next morning will be rejected at send time, while the human agent tag that extends the window exists for a human replying rather than for your bot. Look up the current durations in the published policy rather than trusting this sentence, compute the window from your stored inbound timestamp, and make a closed window a visible conversation state rather than a swallowed error.
A reply you would be happy to have sent
The model answers in your voice from your facts, and stops rather than improvising.
It may quote the persona file and may not extend it.
Files
persona.mdsrc/reply.tsA stop that still sends a message is not a stop.
Files
src/reply.tstests/handover.test.tsThe process restarts and the limit should not.
Files
src/ratelimit.tstests/ratelimit.test.ts
done when · tick each as it passeswatch out- A direct message thread is the easiest place in the world to talk a bot into committing you to something, because there is no interface between the stranger and your prompt, and the reply goes out under your account name with no review. Keep the facts in a file the model may quote and not extend, treat any request for a human as an immediate stop, and store the persona version alongside every reply so that when one goes wrong you know exactly which text produced it.
Tokens, retries and the day it stops working
Credential expiry and delivery failures are noticed by you before they are noticed by a customer.
Confirm the current lifetime in the reference before choosing the margin.
Files
src/tokens.tstests/tokens.test.tsA failed send should be a row you can look at, not a lost message.
Files
src/deliveries.tsOne alert you will actually see beats four dashboards.
Files
src/health.tsdocs/operations.md
done when · tick each as it passeswatch out- A long-lived Instagram user token is valid for sixty days and can only be refreshed while it is at least twenty four hours old and has not yet expired, so a token that lapses cannot be refreshed at all and you are back in the login flow, usually discovering this because somebody mentions the account stopped answering. Verify those durations in the current reference, store the expiry, refresh with a wide margin, and make a failed refresh something that reaches you rather than a line in a log.
Reading back what your bot saidproduct builder
Every automated reply is reviewable, every conversation is pausable, and everything exports.
The screen you would not build is the one that catches the mistakes.
Files
src/digest.tsSo it survives the deploy that broke something.
Files
src/killswitch.tstests/killswitch.test.tsA deletion that leaves rows in the export is not a deletion.
Files
src/export.tstests/export-delete.test.ts
done when · tick each as it passeswatch out- Nobody builds the boring screen where you read yesterday's replies, and without it the first time you learn your bot promised next-day delivery is when somebody asks where their parcel is. Make the digest the default, make the pause reachable in one action from it, and make the off switch a database row so it survives a deployment rather than an environment variable you forget to set.
That is the whole plan for Soccial AI. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Platform app review and serving other accounts.
- Comment automation and comment-to-message flows.
- Outbound campaigns and contact import.
- A customer record system and commerce connectors.
- Anyone-but-you: without Meta App Review your app only serves accounts you add as testers
- Shopify and GoHighLevel context behind replies, and the CRM around the conversations
- Comment-to-DM automations, scheduled follow-ups, and human-approval gates on AI writes
- Someone else babysitting webhook uptime, token refresh, and Meta API deprecations
- A suggested-reply mode where nothing sends until you tap approve, which is the honest first month
- Per-topic handover rules tuned from the handovers table rather than guessed
- A second persona file for a different kind of enquiry, once the first one has survived real messages
Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.