Build Soccial AI

KINDAreplaces $139/mosaves $1,668/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 with Hono, one process, no front endStoragebetter-sqlite3 in a single fileModelOne call per reply with a versioned persona fileExposureAn always-on public endpoint through a tunnel or a small serverDeliverySend through the published messaging endpoint with retries and a dead-letter table

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. free

    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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
IG_ACCESS_TOKENsecretrequiredIGAA...redactedThe long-lived user token. Stored with its expiry and refreshed on a schedule.
IG_ACCOUNT_IDrequired17841400000000000Your own account identifier, used to recognise and ignore your own messages.
META_APP_SECRETsecretrequiredredactedUsed to verify the request signature over the raw body bytes.
META_VERIFY_TOKENsecretrequireda-string-you-chooseEchoed back during the subscription verification handshake.
MODEL_API_KEYsecretrequiredsk-...redactedOne call per reply. Give it a provider-side spend limit too.
DISCLOSURE_TEXTrequiredYou are chatting with an automated assistant.Refuses to start if empty. Check the current platform policy for what it must say.
REDISCLOSE_AFTER_HOURSrequired24Gap after which the disclosure is repeated in an existing thread.
REPLY_MIN_INTERVAL_SECONDSrequired120Per-person rate limit, enforced in the database so a restart does not reset it.

The build, in order

  1. A webhook that survives verification and retries

    Deliveries are authenticated, acknowledged immediately and processed exactly once.

    1. A parsed and re-serialised body is a different byte string.

      Files src/webhook.tstests/signature.test.ts

    2. The platform retries a slow endpoint and you pay for that in duplicate replies.

      Files src/webhook.tssrc/queue.ts

    3. Deduplication belongs in the database, not in an if statement.

      Files src/db/schema.sqltests/dedupe.test.ts

    done when · tick each as it passes
    watch 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.
  2. 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.

    1. And check the current published duration before hard-coding one.

      Files src/window.tstests/window.test.ts

    2. Recorded on the conversation so re-disclosure can be timed.

      Files src/disclosure.tstests/disclosure.test.ts

    3. The one setting that is not a setting.

      Files src/config.ts

    done when · tick each as it passes
    watch 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.
  3. A reply you would be happy to have sent

    The model answers in your voice from your facts, and stops rather than improvising.

    1. It may quote the persona file and may not extend it.

      Files persona.mdsrc/reply.ts

    2. A stop that still sends a message is not a stop.

      Files src/reply.tstests/handover.test.ts

    3. The process restarts and the limit should not.

      Files src/ratelimit.tstests/ratelimit.test.ts

    done when · tick each as it passes
    watch 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.
  4. Tokens, retries and the day it stops working

    Credential expiry and delivery failures are noticed by you before they are noticed by a customer.

    1. Confirm the current lifetime in the reference before choosing the margin.

      Files src/tokens.tstests/tokens.test.ts

    2. A failed send should be a row you can look at, not a lost message.

      Files src/deliveries.ts

    3. One alert you will actually see beats four dashboards.

      Files src/health.tsdocs/operations.md

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.