Vibecode Gojiberry AI
track this build5 phases, 15 steps, beginner friendly0%Every stage of this loop is already a bought part. Signals come from ready-made Apify actors, or from a Hermes agent running the watch on a schedule. Finding and enriching the people behind them is a single call to MoltSets or Prospeo. The LinkedIn send is Unipile, which will hold several connected accounts at once, so the DIY version is not capped at the two senders Pro gives you. What you actually write is the glue, the ICP scoring, and the send queue, and that is one sitting. The asterisk: you are renting four services instead of writing them, and the outreach still runs on real LinkedIn accounts with real limits.
You are building a lean indie version of Gojiberry AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Gojiberry AI · indie build A watcher that polls public signals such as posts, commenters and job changes, scores each person against a written ideal customer profile, enriches only the ones worth contacting, drafts a short note citing the exact signal that fired, and queues it for a human to review and send within safe daily limits. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite through better-sqlite3 | a polling loop and a queue; the whole dataset fits comfortably in one file | | Signals | Public sources you are permitted to read, polled on a schedule | the signal is the product, and where it comes from determines what you are allowed to do with it | | Scoring | A written profile turned into deterministic rules, with a model only for the judgement calls | a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust | | Enrichment | One provider, called only for people who already passed scoring | enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value | | Drafting | A model given the specific signal and your own examples | the note has to cite why you are writing, or it is indistinguishable from every other automated message | | Sending | A review queue for a human, with per-day limits enforced in the application | see rule zero | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the watcher, the scorer and the queue. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A signal source whose terms you have read** · varies by source; some are free and some are priced per run - Why: This decision constrains the entire build and determines whether you are allowed to run it at all. Making it after building is the expensive order. - Get it: Choose the source, read its terms of service and any developer policy, and write down what is permitted with the date. If the terms forbid automated collection, choose a different source rather than proceeding quietly. - Verify: A written note names the source, the permitted use and the date checked - [ ] **An enrichment provider account** · typically priced per lookup or in credit bundles; check the current rate and expect a real miss rate on harder contacts - Why: Turns a name into contact details. Without it you have a scored list and no way to reach anyone. - Get it: Create an account with one provider, take an API key, and read its pricing per lookup and its coverage claims. Start with the smallest paid tier rather than an annual commitment. - Verify: A lookup for a person you can verify returns correct details - [ ] **A model API key** · pay per token; drafting a short note is a fraction of a cent - Why: Used for the judgement part of scoring and for drafting. Both are small calls, so the cost here is modest compared with enrichment. - Get it: Create an account with a provider, generate a key, and set a spend limit in the dashboard at the same time. - Verify: A test draft returns text and appears in the provider usage dashboard - [ ] **Two or three of your own messages that got replies** · free - Why: Phase 4 depends on them. They do more for draft quality than any prompt instruction, because they carry your actual voice. - Get it: Go through your sent messages and copy the ones that worked. Remove anything identifying about the recipient. - Verify: The examples file contains at least two real messages with recipients anonymised - [ ] **A written ideal customer profile** · free - Why: Scoring cannot be built against a vague sense of who is a good fit. Rules need thresholds. - Get it: Write company size, roles, seniority, industries and hard disqualifiers. One page. If you cannot write it, the scoring phase will produce numbers nobody trusts. - Verify: The profile exists as a configuration file with explicit thresholds ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. - one enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner - cross-customer benchmarking and the weekly self-tuning - the ten-minute setup: your version does not exist until you build it - someone else absorbing the breakage when an actor or a LinkedIn endpoint changes - a support line when a sending account gets restricted If one of those is essential to you, that is the reason to keep paying for Gojiberry AI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Gojiberry AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a signal watcher that spots buying intent, scores people against your ideal customer profile and drafts outreach for you to send to replace Gojiberry AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Read this before choosing an architecture, because it decides the shape of the whole build. LinkedIn's User Agreement prohibits bots, scripts and automated activity on the service outright. Enforcement tightened materially through 2026: a working ceiling of roughly 100 connection requests per rolling seven day window, behavioural scoring that varies limits by account age and acceptance rate, and escalation from throttling to permanent account loss and vendor level takedowns. One industry analysis in early 2026 put the share of accounts on non-compliant tools that picked up restrictions at around forty percent. So this build automates the research and drafts the message, and stops there. A human presses send, inside documented limits, from a normal session. That boundary is not timidity; it is the difference between a tool that compounds and an account you cannot get back. Checked September 2026. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite through better-sqlite3 · a polling loop and a queue; the whole dataset fits comfortably in one file - Signals: Public sources you are permitted to read, polled on a schedule · the signal is the product, and where it comes from determines what you are allowed to do with it - Scoring: A written profile turned into deterministic rules, with a model only for the judgement calls · a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust - Enrichment: One provider, called only for people who already passed scoring · enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value - Drafting: A model given the specific signal and your own examples · the note has to cite why you are writing, or it is indistinguishable from every other automated message - Sending: A review queue for a human, with per-day limits enforced in the application · see rule zero ### Data model (create this before Phase 1) - watches(id, kind, target, cadence_minutes, last_polled_at, enabled) · kind is post_engagement, job_change or keyword - signals(id, watch_id, person_ref, kind, evidence_text, evidence_url, observed_at) · evidence is what you will quote in the note - people(id, person_ref, name, headline, company, enriched_at, enrichment_json) - scores(person_id, signal_id, score, reasons_json, scored_at) · reasons must be human readable - drafts(id, person_id, signal_id, body, state, approved_at, sent_at, sent_by) · state is drafted, approved, sent, skipped - send_log(day, channel, count) · enforces the daily cap in the application rather than in your memory ### Phase 1 · One signal source, polled politely Build: Pick one signal source and confirm what its terms permit before writing the poller. Poll on a schedule with backoff, deduplicate by person and signal, and store the evidence text and its URL, because the evidence is what makes the eventual note specific rather than generic. Trap: Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. Done when: A poll produces signal rows with evidence text and a working URL. Polling twice does not duplicate a signal. The permitted use of the chosen source is written down with the date it was checked. A source failure backs off rather than hammering. Do not build yet: No scoring, no enrichment, no drafting. ### Phase 2 · Scoring you can argue with Build: Write the ideal customer profile as explicit rules: company size, role, seniority, industry and disqualifiers. Score deterministically where you can and use a model only for judgement that rules cannot express. Store the reasons alongside the score. Trap: A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. Done when: Scoring a signal produces a number and a list of reasons naming which rules matched. Changing one rule changes the score in a predictable direction. A person matching a disqualifier scores zero regardless of everything else. Ten hand-labelled examples are scored and the disagreements are examined rather than averaged away. Do not build yet: No enrichment yet, so scoring uses only what the signal carried. ### Phase 3 · Enrichment, spent only where it counts Build: Call the enrichment provider only for people above your threshold. Cache every result with its date. Track cost per enriched person and per eventual reply, and handle the common case where the provider simply has nothing. Trap: Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. Done when: Enrichment runs only for people above the threshold, verified by comparing the enrichment call count to the number of qualifying people. A cached person is not re-enriched within the cache window. A provider miss is recorded as a miss and the person continues through the pipeline without contact details. Cost per enriched person is reported. Do not build yet: No drafting yet. ### Phase 4 · Drafts that quote the signal Build: Generate a short note from the evidence text, the person's details and two or three of your own real messages as examples. Require the draft to reference the specific signal. Reject and regenerate any draft that does not, and keep a hard length limit. Trap: A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. Done when: A draft references the specific evidence, verified by a check that some distinctive phrase from the evidence appears in the note. A draft that fails that check is regenerated once and then flagged for manual writing rather than sent. Every draft is within the length limit. Ten drafts read as ten different messages rather than one template. Do not build yet: Nothing is sent yet. That is deliberately the next phase and it is the constrained one. ### Phase 5 · A review queue with limits that bind Build: A queue where each draft is approved, edited or skipped by a person. Approved messages are copied out for sending from a normal session. Enforce a daily cap and a rolling seven day cap in the application, defaulting well below the observed ceilings, and lower still for a new account. Trap: The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. Done when: No code path in the application sends a message. The queue records who approved each draft and when. Reaching the daily cap blocks further approvals with a message naming the reset time. The rolling seven day count is enforced independently of the daily count. The configured caps are below the observed platform ceilings and are documented. Do not build yet: Measurement is the last phase. ### Phase 6 · Measuring whether it works Build: Record outcomes: sent, replied, positive. Break reply rate down by signal type, because the whole premise is that some signals indicate intent and others do not. Retire the watches that do not produce replies. Write down your data handling and deletion policy. Trap: An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. Done when: Reply rate is reported per signal type across at least fifty sent messages. At least one watch has been retired on the evidence. A written policy states what personal data is stored, for how long and how someone is removed on request, and a removal has been tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Automated sending on LinkedIn. It violates the User Agreement, and 2026 enforcement escalated to permanent account loss. The account is worth more than the automation. - A fifteen provider enrichment waterfall. One provider will miss a real share of contacts, and closing that gap is a per-provider integration and a per-provider bill. - Cross-customer benchmarking and the self-tuning that comes from seeing many accounts. You have one account's data and it will take months to say anything. - The ten minute setup. Yours does not exist until you build it, and then it is yours to maintain. ### README must contain - That automating sending on LinkedIn violates its User Agreement and risks permanent account loss, and that this build deliberately stops at drafting - The configured daily and rolling seven day caps, and that platform limits vary by account age and acceptance rate - Which signal source is used and what its terms permit, with the date checked - That enrichment runs only above the score threshold, and the cost per enriched person - What personal data is stored, for how long, and how to remove someone on request ===== AGENTS.md ===== # Agent instructions · Gojiberry AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with SQLite through better-sqlite3, Public sources you are permitted to read, polled on a schedule, A written profile turned into deterministic rules, with a model only for the judgement calls, One provider, called only for people who already passed scoring, A model given the specific signal and your own examples, A review queue for a human, with per-day limits enforced in the application. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ===== BUILD_PLAN.md ===== # Build plan · Gojiberry AI A watcher that polls public signals such as posts, commenters and job changes, scores each person against a written ideal customer profile, enriches only the ones worth contacting, drafts a short note citing the exact signal that fired, and queues it for a human to review and send within safe daily limits. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One signal source, polled politely Real signals land in your database from a source you are permitted to poll. ### Steps 1. Choose the source and record what it permits Write a short note naming the source, what its terms allow and the date you checked. This document is the foundation of everything else. Files: `docs/sources.md` 2. Poll with backoff and deduplication A scheduled poll, exponential backoff on failure, and a unique constraint on person plus signal plus day so re-polling is a no-op. Files: `src/watch.js` 3. Store the evidence, not just the fact The text and the link. A note saying you saw someone engage is generic; a note quoting what they said is not. Files: `src/signals.js` ### Done when - [ ] A poll produces signals with evidence text and a working URL - [ ] Polling twice creates no duplicate rows - [ ] A source failure backs off rather than retrying immediately - [ ] The permitted use of the source is documented with a check date ### Watch out - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. ## Phase 2 · Scoring you can argue with Each person gets a score and a readable list of reasons for it. ### Steps 1. Write the profile as rules Positive signals, negative signals and hard disqualifiers, in a configuration file rather than in code. Files: `config/icp.json` 2. Score deterministically, then add judgement Rules first. A model call only for what rules cannot express, and require it to return reasons. Files: `src/score.js` 3. Label ten examples by hand Score them yourself, then compare. The disagreements tell you which rule is wrong, which is the only way this improves. Files: `eval/labelled.json` ### Done when - [ ] A score comes with readable reasons naming matched rules - [ ] A disqualifier forces a zero regardless of other matches - [ ] Changing one rule moves scores predictably - [ ] The hand-labelled set is scored and disagreements are recorded ### Watch out - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. ## Phase 3 · Enrichment, spent only where it counts You pay for contact data only for people who already passed scoring. ### Steps 1. Gate enrichment behind the score One place in the pipeline where the threshold is applied. Easy to get wrong and expensive when it is. Files: `src/enrich.js` 2. Cache with a freshness window People change jobs, so a cached record has a shelf life. Record when it was fetched and re-enrich after that. Files: `src/enrich.js` 3. Track cost honestly Cost per enriched person, and cost per reply once you have replies. The second number is the one that decides whether this works. Files: `src/cost.js` ### Done when - [ ] Enrichment calls match the number of qualifying people, not the number of signals - [ ] A cached person is not re-enriched within the window - [ ] A provider miss is recorded and does not halt the pipeline - [ ] Cost per enriched person is reported ### Watch out - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. ## Phase 4 · Drafts that quote the signal Each draft names the specific thing that prompted it, and reads like a person wrote it. ### Steps 1. Give the model your own examples Two or three messages you actually sent that got replies. This does more for quality than any amount of prompt instruction. Files: `config/examples.md` 2. Validate that the evidence was used Check for a distinctive token from the evidence in the draft. Regenerate once on failure, then hand it to a human. Files: `src/draft.js` 3. Read ten in a row If they blur together, the prompt is producing a template. Fix that before anything is sent to a real person. ### Done when - [ ] Each draft references its specific evidence, checked programmatically - [ ] A draft failing the check is flagged rather than sent - [ ] Every draft is within the length limit - [ ] Ten drafts read as ten distinct messages ### Watch out - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. ## Phase 5 · A review queue with limits that bind Nothing is sent automatically, and the daily limit is enforced by the application rather than by your memory. ### Steps 1. Build the review queue Approve, edit or skip. Editing is the common case and should be as fast as approving. Files: `src/queue.js` 2. Enforce both caps in code A daily count and a rolling seven day count, both checked before an approval is allowed. Not a warning, a refusal. Files: `src/limits.js` 3. Prove there is no send path Grep the codebase for any outbound send. The absence is a feature and worth asserting in a test. Files: `test/no-send.test.js` ### Done when - [ ] No code path sends a message, asserted by a test - [ ] The daily cap blocks approvals and names the reset time - [ ] The rolling seven day cap is enforced separately - [ ] Every approval records who did it and when ### Watch out - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ## Not in this build - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. ## After v1, if you want it - A second enrichment provider as a fallback, once you know your miss rate - A weekly digest of the highest scoring signals, so review happens in one sitting - Suppression lists for existing customers and people who asked not to be contacted ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite file holding watches, signals, people, scores and drafts. This is the backup. DB_PATH=./data/outreach.db # Required · secret. Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. ENRICHMENT_API_KEY=replace-with-provider-key # Required · secret. Your model provider console. Set a spend limit there as well. MODEL_API_KEY=sk-... # Required. Minimum score before enrichment is spent. Raising it cuts cost and coverage together. SCORE_THRESHOLD=70 # Required. How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. DAILY_APPROVAL_CAP=20 # Required. Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. WEEKLY_APPROVAL_CAP=100 # Required. How long an enrichment result stays fresh before a re-lookup is allowed. ENRICHMENT_CACHE_DAYS=90
You are building a lean indie version of Gojiberry AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Gojiberry AI · indie build A watcher that polls public signals such as posts, commenters and job changes, scores each person against a written ideal customer profile, enriches only the ones worth contacting, drafts a short note citing the exact signal that fired, and queues it for a human to review and send within safe daily limits. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite through better-sqlite3 | a polling loop and a queue; the whole dataset fits comfortably in one file | | Signals | Public sources you are permitted to read, polled on a schedule | the signal is the product, and where it comes from determines what you are allowed to do with it | | Scoring | A written profile turned into deterministic rules, with a model only for the judgement calls | a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust | | Enrichment | One provider, called only for people who already passed scoring | enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value | | Drafting | A model given the specific signal and your own examples | the note has to cite why you are writing, or it is indistinguishable from every other automated message | | Sending | A review queue for a human, with per-day limits enforced in the application | see rule zero | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the watcher, the scorer and the queue. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A signal source whose terms you have read** · varies by source; some are free and some are priced per run - Why: This decision constrains the entire build and determines whether you are allowed to run it at all. Making it after building is the expensive order. - Get it: Choose the source, read its terms of service and any developer policy, and write down what is permitted with the date. If the terms forbid automated collection, choose a different source rather than proceeding quietly. - Verify: A written note names the source, the permitted use and the date checked - [ ] **An enrichment provider account** · typically priced per lookup or in credit bundles; check the current rate and expect a real miss rate on harder contacts - Why: Turns a name into contact details. Without it you have a scored list and no way to reach anyone. - Get it: Create an account with one provider, take an API key, and read its pricing per lookup and its coverage claims. Start with the smallest paid tier rather than an annual commitment. - Verify: A lookup for a person you can verify returns correct details - [ ] **A model API key** · pay per token; drafting a short note is a fraction of a cent - Why: Used for the judgement part of scoring and for drafting. Both are small calls, so the cost here is modest compared with enrichment. - Get it: Create an account with a provider, generate a key, and set a spend limit in the dashboard at the same time. - Verify: A test draft returns text and appears in the provider usage dashboard - [ ] **Two or three of your own messages that got replies** · free - Why: Phase 4 depends on them. They do more for draft quality than any prompt instruction, because they carry your actual voice. - Get it: Go through your sent messages and copy the ones that worked. Remove anything identifying about the recipient. - Verify: The examples file contains at least two real messages with recipients anonymised - [ ] **A written ideal customer profile** · free - Why: Scoring cannot be built against a vague sense of who is a good fit. Rules need thresholds. - Get it: Write company size, roles, seniority, industries and hard disqualifiers. One page. If you cannot write it, the scoring phase will produce numbers nobody trusts. - Verify: The profile exists as a configuration file with explicit thresholds ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. - one enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner - cross-customer benchmarking and the weekly self-tuning - the ten-minute setup: your version does not exist until you build it - someone else absorbing the breakage when an actor or a LinkedIn endpoint changes - a support line when a sending account gets restricted If one of those is essential to you, that is the reason to keep paying for Gojiberry AI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Gojiberry AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a signal watcher that spots buying intent, scores people against your ideal customer profile and drafts outreach for you to send to replace Gojiberry AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Read this before choosing an architecture, because it decides the shape of the whole build. LinkedIn's User Agreement prohibits bots, scripts and automated activity on the service outright. Enforcement tightened materially through 2026: a working ceiling of roughly 100 connection requests per rolling seven day window, behavioural scoring that varies limits by account age and acceptance rate, and escalation from throttling to permanent account loss and vendor level takedowns. One industry analysis in early 2026 put the share of accounts on non-compliant tools that picked up restrictions at around forty percent. So this build automates the research and drafts the message, and stops there. A human presses send, inside documented limits, from a normal session. That boundary is not timidity; it is the difference between a tool that compounds and an account you cannot get back. Checked September 2026. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite through better-sqlite3 · a polling loop and a queue; the whole dataset fits comfortably in one file - Signals: Public sources you are permitted to read, polled on a schedule · the signal is the product, and where it comes from determines what you are allowed to do with it - Scoring: A written profile turned into deterministic rules, with a model only for the judgement calls · a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust - Enrichment: One provider, called only for people who already passed scoring · enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value - Drafting: A model given the specific signal and your own examples · the note has to cite why you are writing, or it is indistinguishable from every other automated message - Sending: A review queue for a human, with per-day limits enforced in the application · see rule zero ### Data model (create this before Phase 1) - watches(id, kind, target, cadence_minutes, last_polled_at, enabled) · kind is post_engagement, job_change or keyword - signals(id, watch_id, person_ref, kind, evidence_text, evidence_url, observed_at) · evidence is what you will quote in the note - people(id, person_ref, name, headline, company, enriched_at, enrichment_json) - scores(person_id, signal_id, score, reasons_json, scored_at) · reasons must be human readable - drafts(id, person_id, signal_id, body, state, approved_at, sent_at, sent_by) · state is drafted, approved, sent, skipped - send_log(day, channel, count) · enforces the daily cap in the application rather than in your memory ### Phase 1 · One signal source, polled politely Build: Pick one signal source and confirm what its terms permit before writing the poller. Poll on a schedule with backoff, deduplicate by person and signal, and store the evidence text and its URL, because the evidence is what makes the eventual note specific rather than generic. Trap: Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. Done when: A poll produces signal rows with evidence text and a working URL. Polling twice does not duplicate a signal. The permitted use of the chosen source is written down with the date it was checked. A source failure backs off rather than hammering. Do not build yet: No scoring, no enrichment, no drafting. ### Phase 2 · Scoring you can argue with Build: Write the ideal customer profile as explicit rules: company size, role, seniority, industry and disqualifiers. Score deterministically where you can and use a model only for judgement that rules cannot express. Store the reasons alongside the score. Trap: A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. Done when: Scoring a signal produces a number and a list of reasons naming which rules matched. Changing one rule changes the score in a predictable direction. A person matching a disqualifier scores zero regardless of everything else. Ten hand-labelled examples are scored and the disagreements are examined rather than averaged away. Do not build yet: No enrichment yet, so scoring uses only what the signal carried. ### Phase 3 · Enrichment, spent only where it counts Build: Call the enrichment provider only for people above your threshold. Cache every result with its date. Track cost per enriched person and per eventual reply, and handle the common case where the provider simply has nothing. Trap: Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. Done when: Enrichment runs only for people above the threshold, verified by comparing the enrichment call count to the number of qualifying people. A cached person is not re-enriched within the cache window. A provider miss is recorded as a miss and the person continues through the pipeline without contact details. Cost per enriched person is reported. Do not build yet: No drafting yet. ### Phase 4 · Drafts that quote the signal Build: Generate a short note from the evidence text, the person's details and two or three of your own real messages as examples. Require the draft to reference the specific signal. Reject and regenerate any draft that does not, and keep a hard length limit. Trap: A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. Done when: A draft references the specific evidence, verified by a check that some distinctive phrase from the evidence appears in the note. A draft that fails that check is regenerated once and then flagged for manual writing rather than sent. Every draft is within the length limit. Ten drafts read as ten different messages rather than one template. Do not build yet: Nothing is sent yet. That is deliberately the next phase and it is the constrained one. ### Phase 5 · A review queue with limits that bind Build: A queue where each draft is approved, edited or skipped by a person. Approved messages are copied out for sending from a normal session. Enforce a daily cap and a rolling seven day cap in the application, defaulting well below the observed ceilings, and lower still for a new account. Trap: The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. Done when: No code path in the application sends a message. The queue records who approved each draft and when. Reaching the daily cap blocks further approvals with a message naming the reset time. The rolling seven day count is enforced independently of the daily count. The configured caps are below the observed platform ceilings and are documented. Do not build yet: Measurement is the last phase. ### Phase 6 · Measuring whether it works Build: Record outcomes: sent, replied, positive. Break reply rate down by signal type, because the whole premise is that some signals indicate intent and others do not. Retire the watches that do not produce replies. Write down your data handling and deletion policy. Trap: An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. Done when: Reply rate is reported per signal type across at least fifty sent messages. At least one watch has been retired on the evidence. A written policy states what personal data is stored, for how long and how someone is removed on request, and a removal has been tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Automated sending on LinkedIn. It violates the User Agreement, and 2026 enforcement escalated to permanent account loss. The account is worth more than the automation. - A fifteen provider enrichment waterfall. One provider will miss a real share of contacts, and closing that gap is a per-provider integration and a per-provider bill. - Cross-customer benchmarking and the self-tuning that comes from seeing many accounts. You have one account's data and it will take months to say anything. - The ten minute setup. Yours does not exist until you build it, and then it is yours to maintain. ### README must contain - That automating sending on LinkedIn violates its User Agreement and risks permanent account loss, and that this build deliberately stops at drafting - The configured daily and rolling seven day caps, and that platform limits vary by account age and acceptance rate - Which signal source is used and what its terms permit, with the date checked - That enrichment runs only above the score threshold, and the cost per enriched person - What personal data is stored, for how long, and how to remove someone on request ===== AGENTS.md ===== # Agent instructions · Gojiberry AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with SQLite through better-sqlite3, Public sources you are permitted to read, polled on a schedule, A written profile turned into deterministic rules, with a model only for the judgement calls, One provider, called only for people who already passed scoring, A model given the specific signal and your own examples, A review queue for a human, with per-day limits enforced in the application. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ===== BUILD_PLAN.md ===== # Build plan · Gojiberry AI A watcher that polls public signals such as posts, commenters and job changes, scores each person against a written ideal customer profile, enriches only the ones worth contacting, drafts a short note citing the exact signal that fired, and queues it for a human to review and send within safe daily limits. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One signal source, polled politely Real signals land in your database from a source you are permitted to poll. ### Steps 1. Choose the source and record what it permits Write a short note naming the source, what its terms allow and the date you checked. This document is the foundation of everything else. Files: `docs/sources.md` 2. Poll with backoff and deduplication A scheduled poll, exponential backoff on failure, and a unique constraint on person plus signal plus day so re-polling is a no-op. Files: `src/watch.js` 3. Store the evidence, not just the fact The text and the link. A note saying you saw someone engage is generic; a note quoting what they said is not. Files: `src/signals.js` ### Done when - [ ] A poll produces signals with evidence text and a working URL - [ ] Polling twice creates no duplicate rows - [ ] A source failure backs off rather than retrying immediately - [ ] The permitted use of the source is documented with a check date ### Watch out - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. ## Phase 2 · Scoring you can argue with Each person gets a score and a readable list of reasons for it. ### Steps 1. Write the profile as rules Positive signals, negative signals and hard disqualifiers, in a configuration file rather than in code. Files: `config/icp.json` 2. Score deterministically, then add judgement Rules first. A model call only for what rules cannot express, and require it to return reasons. Files: `src/score.js` 3. Label ten examples by hand Score them yourself, then compare. The disagreements tell you which rule is wrong, which is the only way this improves. Files: `eval/labelled.json` ### Done when - [ ] A score comes with readable reasons naming matched rules - [ ] A disqualifier forces a zero regardless of other matches - [ ] Changing one rule moves scores predictably - [ ] The hand-labelled set is scored and disagreements are recorded ### Watch out - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. ## Phase 3 · Enrichment, spent only where it counts You pay for contact data only for people who already passed scoring. ### Steps 1. Gate enrichment behind the score One place in the pipeline where the threshold is applied. Easy to get wrong and expensive when it is. Files: `src/enrich.js` 2. Cache with a freshness window People change jobs, so a cached record has a shelf life. Record when it was fetched and re-enrich after that. Files: `src/enrich.js` 3. Track cost honestly Cost per enriched person, and cost per reply once you have replies. The second number is the one that decides whether this works. Files: `src/cost.js` ### Done when - [ ] Enrichment calls match the number of qualifying people, not the number of signals - [ ] A cached person is not re-enriched within the window - [ ] A provider miss is recorded and does not halt the pipeline - [ ] Cost per enriched person is reported ### Watch out - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. ## Phase 4 · Drafts that quote the signal Each draft names the specific thing that prompted it, and reads like a person wrote it. ### Steps 1. Give the model your own examples Two or three messages you actually sent that got replies. This does more for quality than any amount of prompt instruction. Files: `config/examples.md` 2. Validate that the evidence was used Check for a distinctive token from the evidence in the draft. Regenerate once on failure, then hand it to a human. Files: `src/draft.js` 3. Read ten in a row If they blur together, the prompt is producing a template. Fix that before anything is sent to a real person. ### Done when - [ ] Each draft references its specific evidence, checked programmatically - [ ] A draft failing the check is flagged rather than sent - [ ] Every draft is within the length limit - [ ] Ten drafts read as ten distinct messages ### Watch out - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. ## Phase 5 · A review queue with limits that bind Nothing is sent automatically, and the daily limit is enforced by the application rather than by your memory. ### Steps 1. Build the review queue Approve, edit or skip. Editing is the common case and should be as fast as approving. Files: `src/queue.js` 2. Enforce both caps in code A daily count and a rolling seven day count, both checked before an approval is allowed. Not a warning, a refusal. Files: `src/limits.js` 3. Prove there is no send path Grep the codebase for any outbound send. The absence is a feature and worth asserting in a test. Files: `test/no-send.test.js` ### Done when - [ ] No code path sends a message, asserted by a test - [ ] The daily cap blocks approvals and names the reset time - [ ] The rolling seven day cap is enforced separately - [ ] Every approval records who did it and when ### Watch out - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ## Not in this build - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. ## After v1, if you want it - A second enrichment provider as a fallback, once you know your miss rate - A weekly digest of the highest scoring signals, so review happens in one sitting - Suppression lists for existing customers and people who asked not to be contacted ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite file holding watches, signals, people, scores and drafts. This is the backup. DB_PATH=./data/outreach.db # Required · secret. Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. ENRICHMENT_API_KEY=replace-with-provider-key # Required · secret. Your model provider console. Set a spend limit there as well. MODEL_API_KEY=sk-... # Required. Minimum score before enrichment is spent. Raising it cuts cost and coverage together. SCORE_THRESHOLD=70 # Required. How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. DAILY_APPROVAL_CAP=20 # Required. Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. WEEKLY_APPROVAL_CAP=100 # Required. How long an enrichment result stays fresh before a re-lookup is allowed. ENRICHMENT_CACHE_DAYS=90
You are building a production product version of Gojiberry AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Gojiberry AI · product brief ## Problem Every stage of this loop is already a bought part. Signals come from ready-made Apify actors, or from a Hermes agent running the watch on a schedule. Finding and enriching the people behind them is a single call to MoltSets or Prospeo. The LinkedIn send is Unipile, which will hold several connected accounts at once, so the DIY version is not capped at the two senders Pro gives you. What you actually write is the glue, the ICP scoring, and the send queue, and that is one sitting. The asterisk: you are renting four services instead of writing them, and the outreach still runs on real LinkedIn accounts with real limits. ## Product outcome A research and drafting pipeline that finds people showing real intent, explains why it picked them, drafts a note that quotes the signal, and hands it to a person to send inside limits the application enforces, with reply rate measured per signal type. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a signal source: Apify actors, or a Hermes agent running the watch on a schedule - an enrichment provider: MoltSets or Prospeo - Unipile account with one or more LinkedIn accounts connected - OpenAI/Anthropic API key - Node with SQLite (better-sqlite3) ## Explicit non-goals for v1 - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. - one enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner - cross-customer benchmarking and the weekly self-tuning - the ten-minute setup: your version does not exist until you build it - someone else absorbing the breakage when an actor or a LinkedIn endpoint changes - a support line when a sending account gets restricted ## Success criteria - The test asserting no send path exists still passes - Both approval caps are enforced and tested by hitting them - Every draft cites its evidence, checked programmatically - A removal request is proved to delete signals, scores and drafts - The source terms note is current and dated ===== BRIEF.md ===== # Build brief · Gojiberry AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a signal watcher that spots buying intent, scores people against your ideal customer profile and drafts outreach for you to send to replace Gojiberry AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Read this before choosing an architecture, because it decides the shape of the whole build. LinkedIn's User Agreement prohibits bots, scripts and automated activity on the service outright. Enforcement tightened materially through 2026: a working ceiling of roughly 100 connection requests per rolling seven day window, behavioural scoring that varies limits by account age and acceptance rate, and escalation from throttling to permanent account loss and vendor level takedowns. One industry analysis in early 2026 put the share of accounts on non-compliant tools that picked up restrictions at around forty percent. So this build automates the research and drafts the message, and stops there. A human presses send, inside documented limits, from a normal session. That boundary is not timidity; it is the difference between a tool that compounds and an account you cannot get back. Checked September 2026. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite through better-sqlite3 · a polling loop and a queue; the whole dataset fits comfortably in one file - Signals: Public sources you are permitted to read, polled on a schedule · the signal is the product, and where it comes from determines what you are allowed to do with it - Scoring: A written profile turned into deterministic rules, with a model only for the judgement calls · a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust - Enrichment: One provider, called only for people who already passed scoring · enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value - Drafting: A model given the specific signal and your own examples · the note has to cite why you are writing, or it is indistinguishable from every other automated message - Sending: A review queue for a human, with per-day limits enforced in the application · see rule zero ### Data model (create this before Phase 1) - watches(id, kind, target, cadence_minutes, last_polled_at, enabled) · kind is post_engagement, job_change or keyword - signals(id, watch_id, person_ref, kind, evidence_text, evidence_url, observed_at) · evidence is what you will quote in the note - people(id, person_ref, name, headline, company, enriched_at, enrichment_json) - scores(person_id, signal_id, score, reasons_json, scored_at) · reasons must be human readable - drafts(id, person_id, signal_id, body, state, approved_at, sent_at, sent_by) · state is drafted, approved, sent, skipped - send_log(day, channel, count) · enforces the daily cap in the application rather than in your memory ### Phase 1 · One signal source, polled politely Build: Pick one signal source and confirm what its terms permit before writing the poller. Poll on a schedule with backoff, deduplicate by person and signal, and store the evidence text and its URL, because the evidence is what makes the eventual note specific rather than generic. Trap: Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. Done when: A poll produces signal rows with evidence text and a working URL. Polling twice does not duplicate a signal. The permitted use of the chosen source is written down with the date it was checked. A source failure backs off rather than hammering. Do not build yet: No scoring, no enrichment, no drafting. ### Phase 2 · Scoring you can argue with Build: Write the ideal customer profile as explicit rules: company size, role, seniority, industry and disqualifiers. Score deterministically where you can and use a model only for judgement that rules cannot express. Store the reasons alongside the score. Trap: A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. Done when: Scoring a signal produces a number and a list of reasons naming which rules matched. Changing one rule changes the score in a predictable direction. A person matching a disqualifier scores zero regardless of everything else. Ten hand-labelled examples are scored and the disagreements are examined rather than averaged away. Do not build yet: No enrichment yet, so scoring uses only what the signal carried. ### Phase 3 · Enrichment, spent only where it counts Build: Call the enrichment provider only for people above your threshold. Cache every result with its date. Track cost per enriched person and per eventual reply, and handle the common case where the provider simply has nothing. Trap: Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. Done when: Enrichment runs only for people above the threshold, verified by comparing the enrichment call count to the number of qualifying people. A cached person is not re-enriched within the cache window. A provider miss is recorded as a miss and the person continues through the pipeline without contact details. Cost per enriched person is reported. Do not build yet: No drafting yet. ### Phase 4 · Drafts that quote the signal Build: Generate a short note from the evidence text, the person's details and two or three of your own real messages as examples. Require the draft to reference the specific signal. Reject and regenerate any draft that does not, and keep a hard length limit. Trap: A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. Done when: A draft references the specific evidence, verified by a check that some distinctive phrase from the evidence appears in the note. A draft that fails that check is regenerated once and then flagged for manual writing rather than sent. Every draft is within the length limit. Ten drafts read as ten different messages rather than one template. Do not build yet: Nothing is sent yet. That is deliberately the next phase and it is the constrained one. ### Phase 5 · A review queue with limits that bind Build: A queue where each draft is approved, edited or skipped by a person. Approved messages are copied out for sending from a normal session. Enforce a daily cap and a rolling seven day cap in the application, defaulting well below the observed ceilings, and lower still for a new account. Trap: The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. Done when: No code path in the application sends a message. The queue records who approved each draft and when. Reaching the daily cap blocks further approvals with a message naming the reset time. The rolling seven day count is enforced independently of the daily count. The configured caps are below the observed platform ceilings and are documented. Do not build yet: Measurement is the last phase. ### Phase 6 · Measuring whether it works Build: Record outcomes: sent, replied, positive. Break reply rate down by signal type, because the whole premise is that some signals indicate intent and others do not. Retire the watches that do not produce replies. Write down your data handling and deletion policy. Trap: An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. Done when: Reply rate is reported per signal type across at least fifty sent messages. At least one watch has been retired on the evidence. A written policy states what personal data is stored, for how long and how someone is removed on request, and a removal has been tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Automated sending on LinkedIn. It violates the User Agreement, and 2026 enforcement escalated to permanent account loss. The account is worth more than the automation. - A fifteen provider enrichment waterfall. One provider will miss a real share of contacts, and closing that gap is a per-provider integration and a per-provider bill. - Cross-customer benchmarking and the self-tuning that comes from seeing many accounts. You have one account's data and it will take months to say anything. - The ten minute setup. Yours does not exist until you build it, and then it is yours to maintain. ### README must contain - That automating sending on LinkedIn violates its User Agreement and risks permanent account loss, and that this build deliberately stops at drafting - The configured daily and rolling seven day caps, and that platform limits vary by account age and acceptance rate - Which signal source is used and what its terms permit, with the date checked - That enrichment runs only above the score threshold, and the cost per enriched person - What personal data is stored, for how long, and how to remove someone on request ===== ARCHITECTURE.md ===== # Architecture · Gojiberry AI ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite through better-sqlite3 | a polling loop and a queue; the whole dataset fits comfortably in one file | | Signals | Public sources you are permitted to read, polled on a schedule | the signal is the product, and where it comes from determines what you are allowed to do with it | | Scoring | A written profile turned into deterministic rules, with a model only for the judgement calls | a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust | | Enrichment | One provider, called only for people who already passed scoring | enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value | | Drafting | A model given the specific signal and your own examples | the note has to cite why you are writing, or it is indistinguishable from every other automated message | | Sending | A review queue for a human, with per-day limits enforced in the application | see rule zero | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Watch | Polling sources and recording signals with evidence. | Each source is an adapter. Swapping one means re-reading its terms, which is why the terms note lives next to the code. | | Score | Turning a profile into a number plus reasons. | Rules are configuration and the model call is one function. Both are replaceable; the requirement that a score comes with reasons is not. | | Enrich | Contact lookup, caching and cost tracking. | Providers are interchangeable behind one interface. Adding a second as a fallback is the natural growth path and doubles the bookkeeping. | | Draft | Generating and validating notes. | Any model. The evidence-citation check is what keeps quality from collapsing when the model changes. | | Queue and limits | Human approval and the caps. | Do not swap this for automated sending. That is the boundary rule zero exists to hold. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · SQLite file holding watches, signals, people, scores and drafts. This is the backup. - `ENRICHMENT_API_KEY` · required, secret · Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. - `MODEL_API_KEY` · required, secret · Your model provider console. Set a spend limit there as well. - `SCORE_THRESHOLD` · required · Minimum score before enrichment is spent. Raising it cuts cost and coverage together. - `DAILY_APPROVAL_CAP` · required · How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. - `WEEKLY_APPROVAL_CAP` · required · Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. - `ENRICHMENT_CACHE_DAYS` · required · How long an enrichment result stays fresh before a re-lookup is allowed. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Gojiberry AI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with SQLite through better-sqlite3, Public sources you are permitted to read, polled on a schedule, A written profile turned into deterministic rules, with a model only for the judgement calls, One provider, called only for people who already passed scoring, A model given the specific signal and your own examples, A review queue for a human, with per-day limits enforced in the application. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. - An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. ===== MILESTONES.md ===== # Delivery milestones · Gojiberry AI Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One signal source, polled politely Real signals land in your database from a source you are permitted to poll. ### Steps 1. Choose the source and record what it permits Write a short note naming the source, what its terms allow and the date you checked. This document is the foundation of everything else. Files: `docs/sources.md` 2. Poll with backoff and deduplication A scheduled poll, exponential backoff on failure, and a unique constraint on person plus signal plus day so re-polling is a no-op. Files: `src/watch.js` 3. Store the evidence, not just the fact The text and the link. A note saying you saw someone engage is generic; a note quoting what they said is not. Files: `src/signals.js` ### Done when - [ ] A poll produces signals with evidence text and a working URL - [ ] Polling twice creates no duplicate rows - [ ] A source failure backs off rather than retrying immediately - [ ] The permitted use of the source is documented with a check date ### Watch out - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. ## M2 · Scoring you can argue with Each person gets a score and a readable list of reasons for it. ### Steps 1. Write the profile as rules Positive signals, negative signals and hard disqualifiers, in a configuration file rather than in code. Files: `config/icp.json` 2. Score deterministically, then add judgement Rules first. A model call only for what rules cannot express, and require it to return reasons. Files: `src/score.js` 3. Label ten examples by hand Score them yourself, then compare. The disagreements tell you which rule is wrong, which is the only way this improves. Files: `eval/labelled.json` ### Done when - [ ] A score comes with readable reasons naming matched rules - [ ] A disqualifier forces a zero regardless of other matches - [ ] Changing one rule moves scores predictably - [ ] The hand-labelled set is scored and disagreements are recorded ### Watch out - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. ## M3 · Enrichment, spent only where it counts You pay for contact data only for people who already passed scoring. ### Steps 1. Gate enrichment behind the score One place in the pipeline where the threshold is applied. Easy to get wrong and expensive when it is. Files: `src/enrich.js` 2. Cache with a freshness window People change jobs, so a cached record has a shelf life. Record when it was fetched and re-enrich after that. Files: `src/enrich.js` 3. Track cost honestly Cost per enriched person, and cost per reply once you have replies. The second number is the one that decides whether this works. Files: `src/cost.js` ### Done when - [ ] Enrichment calls match the number of qualifying people, not the number of signals - [ ] A cached person is not re-enriched within the window - [ ] A provider miss is recorded and does not halt the pipeline - [ ] Cost per enriched person is reported ### Watch out - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. ## M4 · Drafts that quote the signal Each draft names the specific thing that prompted it, and reads like a person wrote it. ### Steps 1. Give the model your own examples Two or three messages you actually sent that got replies. This does more for quality than any amount of prompt instruction. Files: `config/examples.md` 2. Validate that the evidence was used Check for a distinctive token from the evidence in the draft. Regenerate once on failure, then hand it to a human. Files: `src/draft.js` 3. Read ten in a row If they blur together, the prompt is producing a template. Fix that before anything is sent to a real person. ### Done when - [ ] Each draft references its specific evidence, checked programmatically - [ ] A draft failing the check is flagged rather than sent - [ ] Every draft is within the length limit - [ ] Ten drafts read as ten distinct messages ### Watch out - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. ## M5 · A review queue with limits that bind Nothing is sent automatically, and the daily limit is enforced by the application rather than by your memory. ### Steps 1. Build the review queue Approve, edit or skip. Editing is the common case and should be as fast as approving. Files: `src/queue.js` 2. Enforce both caps in code A daily count and a rolling seven day count, both checked before an approval is allowed. Not a warning, a refusal. Files: `src/limits.js` 3. Prove there is no send path Grep the codebase for any outbound send. The absence is a feature and worth asserting in a test. Files: `test/no-send.test.js` ### Done when - [ ] No code path sends a message, asserted by a test - [ ] The daily cap blocks approvals and names the reset time - [ ] The rolling seven day cap is enforced separately - [ ] Every approval records who did it and when ### Watch out - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ## M6 · Measuring whether it works (production only) You know your reply rate by signal type, and you stop watching the signals that do not pay. ### Steps 1. Record outcomes against drafts Sent, replied and whether the reply was positive. Manual entry is fine; the number matters more than the automation. Files: `src/outcomes.js` 2. Report by signal type The breakdown is the decision-making tool. Aggregate reply rate tells you nothing you can act on. Files: `src/report.js` 3. Write and test the data policy You are storing information about people who did not ask you to. Say what you keep, for how long, and remove someone on request. Then test the removal. Files: `docs/data-policy.md` ### Done when - [ ] Reply rate is reported per signal type over at least fifty messages - [ ] A watch has been retired based on the evidence - [ ] A removal request deletes that person's signals, scores and drafts - [ ] The data policy is written and matches what is actually stored ### Watch out - An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. ===== OPERATIONS.md ===== # Operations · Gojiberry AI ## Backup The SQLite file holds everything, including personal data about people who did not opt in. Back it up, and encrypt the backup, because the sensitivity is higher than the size suggests. ## Restore Restore the file and resume polling. Signals older than the cache window will re-enrich, which costs money, so a restore has a bill attached and it is worth knowing that in advance. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Poll failures per source, enrichment spend per week against a budget, and reply rate per signal type. A source that silently starts returning nothing looks identical to a quiet week unless you watch it. ## Incident checklist If a source's terms change, stop that watch first and decide afterwards. If someone asks to be removed, the deletion path from Phase 6 must cover signals, scores and drafts, not just the person row. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The test asserting no send path exists still passes - [ ] Both approval caps are enforced and tested by hitting them - [ ] Every draft cites its evidence, checked programmatically - [ ] A removal request is proved to delete signals, scores and drafts - [ ] The source terms note is current and dated ## Launch constraint Do not market omitted Gojiberry AI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite file holding watches, signals, people, scores and drafts. This is the backup. DB_PATH=./data/outreach.db # Required · secret. Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. ENRICHMENT_API_KEY=replace-with-provider-key # Required · secret. Your model provider console. Set a spend limit there as well. MODEL_API_KEY=sk-... # Required. Minimum score before enrichment is spent. Raising it cuts cost and coverage together. SCORE_THRESHOLD=70 # Required. How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. DAILY_APPROVAL_CAP=20 # Required. Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. WEEKLY_APPROVAL_CAP=100 # Required. How long an enrichment result stays fresh before a re-lookup is allowed. ENRICHMENT_CACHE_DAYS=90
# Gojiberry AI · indie build A watcher that polls public signals such as posts, commenters and job changes, scores each person against a written ideal customer profile, enriches only the ones worth contacting, drafts a short note citing the exact signal that fired, and queues it for a human to review and send within safe daily limits. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite through better-sqlite3 | a polling loop and a queue; the whole dataset fits comfortably in one file | | Signals | Public sources you are permitted to read, polled on a schedule | the signal is the product, and where it comes from determines what you are allowed to do with it | | Scoring | A written profile turned into deterministic rules, with a model only for the judgement calls | a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust | | Enrichment | One provider, called only for people who already passed scoring | enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value | | Drafting | A model given the specific signal and your own examples | the note has to cite why you are writing, or it is indistinguishable from every other automated message | | Sending | A review queue for a human, with per-day limits enforced in the application | see rule zero | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the watcher, the scorer and the queue. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A signal source whose terms you have read** · varies by source; some are free and some are priced per run - Why: This decision constrains the entire build and determines whether you are allowed to run it at all. Making it after building is the expensive order. - Get it: Choose the source, read its terms of service and any developer policy, and write down what is permitted with the date. If the terms forbid automated collection, choose a different source rather than proceeding quietly. - Verify: A written note names the source, the permitted use and the date checked - [ ] **An enrichment provider account** · typically priced per lookup or in credit bundles; check the current rate and expect a real miss rate on harder contacts - Why: Turns a name into contact details. Without it you have a scored list and no way to reach anyone. - Get it: Create an account with one provider, take an API key, and read its pricing per lookup and its coverage claims. Start with the smallest paid tier rather than an annual commitment. - Verify: A lookup for a person you can verify returns correct details - [ ] **A model API key** · pay per token; drafting a short note is a fraction of a cent - Why: Used for the judgement part of scoring and for drafting. Both are small calls, so the cost here is modest compared with enrichment. - Get it: Create an account with a provider, generate a key, and set a spend limit in the dashboard at the same time. - Verify: A test draft returns text and appears in the provider usage dashboard - [ ] **Two or three of your own messages that got replies** · free - Why: Phase 4 depends on them. They do more for draft quality than any prompt instruction, because they carry your actual voice. - Get it: Go through your sent messages and copy the ones that worked. Remove anything identifying about the recipient. - Verify: The examples file contains at least two real messages with recipients anonymised - [ ] **A written ideal customer profile** · free - Why: Scoring cannot be built against a vague sense of who is a good fit. Rules need thresholds. - Get it: Write company size, roles, seniority, industries and hard disqualifiers. One page. If you cannot write it, the scoring phase will produce numbers nobody trusts. - Verify: The profile exists as a configuration file with explicit thresholds ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. - one enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner - cross-customer benchmarking and the weekly self-tuning - the ten-minute setup: your version does not exist until you build it - someone else absorbing the breakage when an actor or a LinkedIn endpoint changes - a support line when a sending account gets restricted If one of those is essential to you, that is the reason to keep paying for Gojiberry AI, and the README should say so rather than pretend.
# Build brief · Gojiberry AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a signal watcher that spots buying intent, scores people against your ideal customer profile and drafts outreach for you to send to replace Gojiberry AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Read this before choosing an architecture, because it decides the shape of the whole build. LinkedIn's User Agreement prohibits bots, scripts and automated activity on the service outright. Enforcement tightened materially through 2026: a working ceiling of roughly 100 connection requests per rolling seven day window, behavioural scoring that varies limits by account age and acceptance rate, and escalation from throttling to permanent account loss and vendor level takedowns. One industry analysis in early 2026 put the share of accounts on non-compliant tools that picked up restrictions at around forty percent. So this build automates the research and drafts the message, and stops there. A human presses send, inside documented limits, from a normal session. That boundary is not timidity; it is the difference between a tool that compounds and an account you cannot get back. Checked September 2026. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite through better-sqlite3 · a polling loop and a queue; the whole dataset fits comfortably in one file - Signals: Public sources you are permitted to read, polled on a schedule · the signal is the product, and where it comes from determines what you are allowed to do with it - Scoring: A written profile turned into deterministic rules, with a model only for the judgement calls · a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust - Enrichment: One provider, called only for people who already passed scoring · enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value - Drafting: A model given the specific signal and your own examples · the note has to cite why you are writing, or it is indistinguishable from every other automated message - Sending: A review queue for a human, with per-day limits enforced in the application · see rule zero ### Data model (create this before Phase 1) - watches(id, kind, target, cadence_minutes, last_polled_at, enabled) · kind is post_engagement, job_change or keyword - signals(id, watch_id, person_ref, kind, evidence_text, evidence_url, observed_at) · evidence is what you will quote in the note - people(id, person_ref, name, headline, company, enriched_at, enrichment_json) - scores(person_id, signal_id, score, reasons_json, scored_at) · reasons must be human readable - drafts(id, person_id, signal_id, body, state, approved_at, sent_at, sent_by) · state is drafted, approved, sent, skipped - send_log(day, channel, count) · enforces the daily cap in the application rather than in your memory ### Phase 1 · One signal source, polled politely Build: Pick one signal source and confirm what its terms permit before writing the poller. Poll on a schedule with backoff, deduplicate by person and signal, and store the evidence text and its URL, because the evidence is what makes the eventual note specific rather than generic. Trap: Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. Done when: A poll produces signal rows with evidence text and a working URL. Polling twice does not duplicate a signal. The permitted use of the chosen source is written down with the date it was checked. A source failure backs off rather than hammering. Do not build yet: No scoring, no enrichment, no drafting. ### Phase 2 · Scoring you can argue with Build: Write the ideal customer profile as explicit rules: company size, role, seniority, industry and disqualifiers. Score deterministically where you can and use a model only for judgement that rules cannot express. Store the reasons alongside the score. Trap: A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. Done when: Scoring a signal produces a number and a list of reasons naming which rules matched. Changing one rule changes the score in a predictable direction. A person matching a disqualifier scores zero regardless of everything else. Ten hand-labelled examples are scored and the disagreements are examined rather than averaged away. Do not build yet: No enrichment yet, so scoring uses only what the signal carried. ### Phase 3 · Enrichment, spent only where it counts Build: Call the enrichment provider only for people above your threshold. Cache every result with its date. Track cost per enriched person and per eventual reply, and handle the common case where the provider simply has nothing. Trap: Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. Done when: Enrichment runs only for people above the threshold, verified by comparing the enrichment call count to the number of qualifying people. A cached person is not re-enriched within the cache window. A provider miss is recorded as a miss and the person continues through the pipeline without contact details. Cost per enriched person is reported. Do not build yet: No drafting yet. ### Phase 4 · Drafts that quote the signal Build: Generate a short note from the evidence text, the person's details and two or three of your own real messages as examples. Require the draft to reference the specific signal. Reject and regenerate any draft that does not, and keep a hard length limit. Trap: A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. Done when: A draft references the specific evidence, verified by a check that some distinctive phrase from the evidence appears in the note. A draft that fails that check is regenerated once and then flagged for manual writing rather than sent. Every draft is within the length limit. Ten drafts read as ten different messages rather than one template. Do not build yet: Nothing is sent yet. That is deliberately the next phase and it is the constrained one. ### Phase 5 · A review queue with limits that bind Build: A queue where each draft is approved, edited or skipped by a person. Approved messages are copied out for sending from a normal session. Enforce a daily cap and a rolling seven day cap in the application, defaulting well below the observed ceilings, and lower still for a new account. Trap: The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. Done when: No code path in the application sends a message. The queue records who approved each draft and when. Reaching the daily cap blocks further approvals with a message naming the reset time. The rolling seven day count is enforced independently of the daily count. The configured caps are below the observed platform ceilings and are documented. Do not build yet: Measurement is the last phase. ### Phase 6 · Measuring whether it works Build: Record outcomes: sent, replied, positive. Break reply rate down by signal type, because the whole premise is that some signals indicate intent and others do not. Retire the watches that do not produce replies. Write down your data handling and deletion policy. Trap: An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. Done when: Reply rate is reported per signal type across at least fifty sent messages. At least one watch has been retired on the evidence. A written policy states what personal data is stored, for how long and how someone is removed on request, and a removal has been tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Automated sending on LinkedIn. It violates the User Agreement, and 2026 enforcement escalated to permanent account loss. The account is worth more than the automation. - A fifteen provider enrichment waterfall. One provider will miss a real share of contacts, and closing that gap is a per-provider integration and a per-provider bill. - Cross-customer benchmarking and the self-tuning that comes from seeing many accounts. You have one account's data and it will take months to say anything. - The ten minute setup. Yours does not exist until you build it, and then it is yours to maintain. ### README must contain - That automating sending on LinkedIn violates its User Agreement and risks permanent account loss, and that this build deliberately stops at drafting - The configured daily and rolling seven day caps, and that platform limits vary by account age and acceptance rate - Which signal source is used and what its terms permit, with the date checked - That enrichment runs only above the score threshold, and the cost per enriched person - What personal data is stored, for how long, and how to remove someone on request
# Agent instructions · Gojiberry AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with SQLite through better-sqlite3, Public sources you are permitted to read, polled on a schedule, A written profile turned into deterministic rules, with a model only for the judgement calls, One provider, called only for people who already passed scoring, A model given the specific signal and your own examples, A review queue for a human, with per-day limits enforced in the application. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling.
# Build plan · Gojiberry AI A watcher that polls public signals such as posts, commenters and job changes, scores each person against a written ideal customer profile, enriches only the ones worth contacting, drafts a short note citing the exact signal that fired, and queues it for a human to review and send within safe daily limits. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One signal source, polled politely Real signals land in your database from a source you are permitted to poll. ### Steps 1. Choose the source and record what it permits Write a short note naming the source, what its terms allow and the date you checked. This document is the foundation of everything else. Files: `docs/sources.md` 2. Poll with backoff and deduplication A scheduled poll, exponential backoff on failure, and a unique constraint on person plus signal plus day so re-polling is a no-op. Files: `src/watch.js` 3. Store the evidence, not just the fact The text and the link. A note saying you saw someone engage is generic; a note quoting what they said is not. Files: `src/signals.js` ### Done when - [ ] A poll produces signals with evidence text and a working URL - [ ] Polling twice creates no duplicate rows - [ ] A source failure backs off rather than retrying immediately - [ ] The permitted use of the source is documented with a check date ### Watch out - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. ## Phase 2 · Scoring you can argue with Each person gets a score and a readable list of reasons for it. ### Steps 1. Write the profile as rules Positive signals, negative signals and hard disqualifiers, in a configuration file rather than in code. Files: `config/icp.json` 2. Score deterministically, then add judgement Rules first. A model call only for what rules cannot express, and require it to return reasons. Files: `src/score.js` 3. Label ten examples by hand Score them yourself, then compare. The disagreements tell you which rule is wrong, which is the only way this improves. Files: `eval/labelled.json` ### Done when - [ ] A score comes with readable reasons naming matched rules - [ ] A disqualifier forces a zero regardless of other matches - [ ] Changing one rule moves scores predictably - [ ] The hand-labelled set is scored and disagreements are recorded ### Watch out - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. ## Phase 3 · Enrichment, spent only where it counts You pay for contact data only for people who already passed scoring. ### Steps 1. Gate enrichment behind the score One place in the pipeline where the threshold is applied. Easy to get wrong and expensive when it is. Files: `src/enrich.js` 2. Cache with a freshness window People change jobs, so a cached record has a shelf life. Record when it was fetched and re-enrich after that. Files: `src/enrich.js` 3. Track cost honestly Cost per enriched person, and cost per reply once you have replies. The second number is the one that decides whether this works. Files: `src/cost.js` ### Done when - [ ] Enrichment calls match the number of qualifying people, not the number of signals - [ ] A cached person is not re-enriched within the window - [ ] A provider miss is recorded and does not halt the pipeline - [ ] Cost per enriched person is reported ### Watch out - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. ## Phase 4 · Drafts that quote the signal Each draft names the specific thing that prompted it, and reads like a person wrote it. ### Steps 1. Give the model your own examples Two or three messages you actually sent that got replies. This does more for quality than any amount of prompt instruction. Files: `config/examples.md` 2. Validate that the evidence was used Check for a distinctive token from the evidence in the draft. Regenerate once on failure, then hand it to a human. Files: `src/draft.js` 3. Read ten in a row If they blur together, the prompt is producing a template. Fix that before anything is sent to a real person. ### Done when - [ ] Each draft references its specific evidence, checked programmatically - [ ] A draft failing the check is flagged rather than sent - [ ] Every draft is within the length limit - [ ] Ten drafts read as ten distinct messages ### Watch out - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. ## Phase 5 · A review queue with limits that bind Nothing is sent automatically, and the daily limit is enforced by the application rather than by your memory. ### Steps 1. Build the review queue Approve, edit or skip. Editing is the common case and should be as fast as approving. Files: `src/queue.js` 2. Enforce both caps in code A daily count and a rolling seven day count, both checked before an approval is allowed. Not a warning, a refusal. Files: `src/limits.js` 3. Prove there is no send path Grep the codebase for any outbound send. The absence is a feature and worth asserting in a test. Files: `test/no-send.test.js` ### Done when - [ ] No code path sends a message, asserted by a test - [ ] The daily cap blocks approvals and names the reset time - [ ] The rolling seven day cap is enforced separately - [ ] Every approval records who did it and when ### Watch out - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ## Not in this build - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. ## After v1, if you want it - A second enrichment provider as a fallback, once you know your miss rate - A weekly digest of the highest scoring signals, so review happens in one sitting - Suppression lists for existing customers and people who asked not to be contacted
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite file holding watches, signals, people, scores and drafts. This is the backup. DB_PATH=./data/outreach.db # Required · secret. Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. ENRICHMENT_API_KEY=replace-with-provider-key # Required · secret. Your model provider console. Set a spend limit there as well. MODEL_API_KEY=sk-... # Required. Minimum score before enrichment is spent. Raising it cuts cost and coverage together. SCORE_THRESHOLD=70 # Required. How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. DAILY_APPROVAL_CAP=20 # Required. Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. WEEKLY_APPROVAL_CAP=100 # Required. How long an enrichment result stays fresh before a re-lookup is allowed. ENRICHMENT_CACHE_DAYS=90
# Gojiberry AI · product brief ## Problem Every stage of this loop is already a bought part. Signals come from ready-made Apify actors, or from a Hermes agent running the watch on a schedule. Finding and enriching the people behind them is a single call to MoltSets or Prospeo. The LinkedIn send is Unipile, which will hold several connected accounts at once, so the DIY version is not capped at the two senders Pro gives you. What you actually write is the glue, the ICP scoring, and the send queue, and that is one sitting. The asterisk: you are renting four services instead of writing them, and the outreach still runs on real LinkedIn accounts with real limits. ## Product outcome A research and drafting pipeline that finds people showing real intent, explains why it picked them, drafts a note that quotes the signal, and hands it to a person to send inside limits the application enforces, with reply rate measured per signal type. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a signal source: Apify actors, or a Hermes agent running the watch on a schedule - an enrichment provider: MoltSets or Prospeo - Unipile account with one or more LinkedIn accounts connected - OpenAI/Anthropic API key - Node with SQLite (better-sqlite3) ## Explicit non-goals for v1 - Automated sending, which is against the platform's User Agreement and risks the account. - A multi-provider enrichment waterfall and the coverage it buys. - Cross-customer benchmarking and self-tuning. - A ten minute setup, since this one is yours to build and maintain. - one enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner - cross-customer benchmarking and the weekly self-tuning - the ten-minute setup: your version does not exist until you build it - someone else absorbing the breakage when an actor or a LinkedIn endpoint changes - a support line when a sending account gets restricted ## Success criteria - The test asserting no send path exists still passes - Both approval caps are enforced and tested by hitting them - Every draft cites its evidence, checked programmatically - A removal request is proved to delete signals, scores and drafts - The source terms note is current and dated
# Build brief · Gojiberry AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a signal watcher that spots buying intent, scores people against your ideal customer profile and drafts outreach for you to send to replace Gojiberry AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Read this before choosing an architecture, because it decides the shape of the whole build. LinkedIn's User Agreement prohibits bots, scripts and automated activity on the service outright. Enforcement tightened materially through 2026: a working ceiling of roughly 100 connection requests per rolling seven day window, behavioural scoring that varies limits by account age and acceptance rate, and escalation from throttling to permanent account loss and vendor level takedowns. One industry analysis in early 2026 put the share of accounts on non-compliant tools that picked up restrictions at around forty percent. So this build automates the research and drafts the message, and stops there. A human presses send, inside documented limits, from a normal session. That boundary is not timidity; it is the difference between a tool that compounds and an account you cannot get back. Checked September 2026. ### Stack (fixed, do not substitute) - Runtime: Node 22 with SQLite through better-sqlite3 · a polling loop and a queue; the whole dataset fits comfortably in one file - Signals: Public sources you are permitted to read, polled on a schedule · the signal is the product, and where it comes from determines what you are allowed to do with it - Scoring: A written profile turned into deterministic rules, with a model only for the judgement calls · a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust - Enrichment: One provider, called only for people who already passed scoring · enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value - Drafting: A model given the specific signal and your own examples · the note has to cite why you are writing, or it is indistinguishable from every other automated message - Sending: A review queue for a human, with per-day limits enforced in the application · see rule zero ### Data model (create this before Phase 1) - watches(id, kind, target, cadence_minutes, last_polled_at, enabled) · kind is post_engagement, job_change or keyword - signals(id, watch_id, person_ref, kind, evidence_text, evidence_url, observed_at) · evidence is what you will quote in the note - people(id, person_ref, name, headline, company, enriched_at, enrichment_json) - scores(person_id, signal_id, score, reasons_json, scored_at) · reasons must be human readable - drafts(id, person_id, signal_id, body, state, approved_at, sent_at, sent_by) · state is drafted, approved, sent, skipped - send_log(day, channel, count) · enforces the daily cap in the application rather than in your memory ### Phase 1 · One signal source, polled politely Build: Pick one signal source and confirm what its terms permit before writing the poller. Poll on a schedule with backoff, deduplicate by person and signal, and store the evidence text and its URL, because the evidence is what makes the eventual note specific rather than generic. Trap: Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. Done when: A poll produces signal rows with evidence text and a working URL. Polling twice does not duplicate a signal. The permitted use of the chosen source is written down with the date it was checked. A source failure backs off rather than hammering. Do not build yet: No scoring, no enrichment, no drafting. ### Phase 2 · Scoring you can argue with Build: Write the ideal customer profile as explicit rules: company size, role, seniority, industry and disqualifiers. Score deterministically where you can and use a model only for judgement that rules cannot express. Store the reasons alongside the score. Trap: A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. Done when: Scoring a signal produces a number and a list of reasons naming which rules matched. Changing one rule changes the score in a predictable direction. A person matching a disqualifier scores zero regardless of everything else. Ten hand-labelled examples are scored and the disagreements are examined rather than averaged away. Do not build yet: No enrichment yet, so scoring uses only what the signal carried. ### Phase 3 · Enrichment, spent only where it counts Build: Call the enrichment provider only for people above your threshold. Cache every result with its date. Track cost per enriched person and per eventual reply, and handle the common case where the provider simply has nothing. Trap: Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. Done when: Enrichment runs only for people above the threshold, verified by comparing the enrichment call count to the number of qualifying people. A cached person is not re-enriched within the cache window. A provider miss is recorded as a miss and the person continues through the pipeline without contact details. Cost per enriched person is reported. Do not build yet: No drafting yet. ### Phase 4 · Drafts that quote the signal Build: Generate a short note from the evidence text, the person's details and two or three of your own real messages as examples. Require the draft to reference the specific signal. Reject and regenerate any draft that does not, and keep a hard length limit. Trap: A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. Done when: A draft references the specific evidence, verified by a check that some distinctive phrase from the evidence appears in the note. A draft that fails that check is regenerated once and then flagged for manual writing rather than sent. Every draft is within the length limit. Ten drafts read as ten different messages rather than one template. Do not build yet: Nothing is sent yet. That is deliberately the next phase and it is the constrained one. ### Phase 5 · A review queue with limits that bind Build: A queue where each draft is approved, edited or skipped by a person. Approved messages are copied out for sending from a normal session. Enforce a daily cap and a rolling seven day cap in the application, defaulting well below the observed ceilings, and lower still for a new account. Trap: The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. Done when: No code path in the application sends a message. The queue records who approved each draft and when. Reaching the daily cap blocks further approvals with a message naming the reset time. The rolling seven day count is enforced independently of the daily count. The configured caps are below the observed platform ceilings and are documented. Do not build yet: Measurement is the last phase. ### Phase 6 · Measuring whether it works Build: Record outcomes: sent, replied, positive. Break reply rate down by signal type, because the whole premise is that some signals indicate intent and others do not. Retire the watches that do not produce replies. Write down your data handling and deletion policy. Trap: An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later. Done when: Reply rate is reported per signal type across at least fifty sent messages. At least one watch has been retired on the evidence. A written policy states what personal data is stored, for how long and how someone is removed on request, and a removal has been tested. Do not build yet: Nothing after this. ### Out of scope (and why) - Automated sending on LinkedIn. It violates the User Agreement, and 2026 enforcement escalated to permanent account loss. The account is worth more than the automation. - A fifteen provider enrichment waterfall. One provider will miss a real share of contacts, and closing that gap is a per-provider integration and a per-provider bill. - Cross-customer benchmarking and the self-tuning that comes from seeing many accounts. You have one account's data and it will take months to say anything. - The ten minute setup. Yours does not exist until you build it, and then it is yours to maintain. ### README must contain - That automating sending on LinkedIn violates its User Agreement and risks permanent account loss, and that this build deliberately stops at drafting - The configured daily and rolling seven day caps, and that platform limits vary by account age and acceptance rate - Which signal source is used and what its terms permit, with the date checked - That enrichment runs only above the score threshold, and the cost per enriched person - What personal data is stored, for how long, and how to remove someone on request
# Architecture · Gojiberry AI ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with SQLite through better-sqlite3 | a polling loop and a queue; the whole dataset fits comfortably in one file | | Signals | Public sources you are permitted to read, polled on a schedule | the signal is the product, and where it comes from determines what you are allowed to do with it | | Scoring | A written profile turned into deterministic rules, with a model only for the judgement calls | a rule you can read is a rule you can fix; a score you cannot explain is a score you cannot trust | | Enrichment | One provider, called only for people who already passed scoring | enrichment is the per-contact cost, so spending it before scoring is how the bill outruns the value | | Drafting | A model given the specific signal and your own examples | the note has to cite why you are writing, or it is indistinguishable from every other automated message | | Sending | A review queue for a human, with per-day limits enforced in the application | see rule zero | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Watch | Polling sources and recording signals with evidence. | Each source is an adapter. Swapping one means re-reading its terms, which is why the terms note lives next to the code. | | Score | Turning a profile into a number plus reasons. | Rules are configuration and the model call is one function. Both are replaceable; the requirement that a score comes with reasons is not. | | Enrich | Contact lookup, caching and cost tracking. | Providers are interchangeable behind one interface. Adding a second as a fallback is the natural growth path and doubles the bookkeeping. | | Draft | Generating and validating notes. | Any model. The evidence-citation check is what keeps quality from collapsing when the model changes. | | Queue and limits | Human approval and the caps. | Do not swap this for automated sending. That is the boundary rule zero exists to hold. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · SQLite file holding watches, signals, people, scores and drafts. This is the backup. - `ENRICHMENT_API_KEY` · required, secret · Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. - `MODEL_API_KEY` · required, secret · Your model provider console. Set a spend limit there as well. - `SCORE_THRESHOLD` · required · Minimum score before enrichment is spent. Raising it cuts cost and coverage together. - `DAILY_APPROVAL_CAP` · required · How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. - `WEEKLY_APPROVAL_CAP` · required · Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. - `ENRICHMENT_CACHE_DAYS` · required · How long an enrichment result stays fresh before a re-lookup is allowed. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Gojiberry AI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with SQLite through better-sqlite3, Public sources you are permitted to read, polled on a schedule, A written profile turned into deterministic rules, with a model only for the judgement calls, One provider, called only for people who already passed scoring, A model given the specific signal and your own examples, A review queue for a human, with per-day limits enforced in the application. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. - An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later.
# Delivery milestones · Gojiberry AI Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One signal source, polled politely Real signals land in your database from a source you are permitted to poll. ### Steps 1. Choose the source and record what it permits Write a short note naming the source, what its terms allow and the date you checked. This document is the foundation of everything else. Files: `docs/sources.md` 2. Poll with backoff and deduplication A scheduled poll, exponential backoff on failure, and a unique constraint on person plus signal plus day so re-polling is a no-op. Files: `src/watch.js` 3. Store the evidence, not just the fact The text and the link. A note saying you saw someone engage is generic; a note quoting what they said is not. Files: `src/signals.js` ### Done when - [ ] A poll produces signals with evidence text and a working URL - [ ] Polling twice creates no duplicate rows - [ ] A source failure backs off rather than retrying immediately - [ ] The permitted use of the source is documented with a check date ### Watch out - Deciding the source after building the pipeline is how people end up with a working system they are not allowed to run. Read the terms of the specific source first, write down what it permits, and let that decide the architecture. ## M2 · Scoring you can argue with Each person gets a score and a readable list of reasons for it. ### Steps 1. Write the profile as rules Positive signals, negative signals and hard disqualifiers, in a configuration file rather than in code. Files: `config/icp.json` 2. Score deterministically, then add judgement Rules first. A model call only for what rules cannot express, and require it to return reasons. Files: `src/score.js` 3. Label ten examples by hand Score them yourself, then compare. The disagreements tell you which rule is wrong, which is the only way this improves. Files: `eval/labelled.json` ### Done when - [ ] A score comes with readable reasons naming matched rules - [ ] A disqualifier forces a zero regardless of other matches - [ ] Changing one rule moves scores predictably - [ ] The hand-labelled set is scored and disagreements are recorded ### Watch out - A single opaque number from a model cannot be debugged, so when it starts surfacing the wrong people you have no lever to pull. Rules produce reasons, reasons produce fixes. Reserve the model for the parts that are genuinely fuzzy, and make it explain itself there too. ## M3 · Enrichment, spent only where it counts You pay for contact data only for people who already passed scoring. ### Steps 1. Gate enrichment behind the score One place in the pipeline where the threshold is applied. Easy to get wrong and expensive when it is. Files: `src/enrich.js` 2. Cache with a freshness window People change jobs, so a cached record has a shelf life. Record when it was fetched and re-enrich after that. Files: `src/enrich.js` 3. Track cost honestly Cost per enriched person, and cost per reply once you have replies. The second number is the one that decides whether this works. Files: `src/cost.js` ### Done when - [ ] Enrichment calls match the number of qualifying people, not the number of signals - [ ] A cached person is not re-enriched within the window - [ ] A provider miss is recorded and does not halt the pipeline - [ ] Cost per enriched person is reported ### Watch out - Enriching every signal before scoring inverts the economics: enrichment is priced per lookup and most signals do not deserve one. A single provider will also miss a meaningful share of people, and a miss is a normal outcome to record rather than an error to retry. ## M4 · Drafts that quote the signal Each draft names the specific thing that prompted it, and reads like a person wrote it. ### Steps 1. Give the model your own examples Two or three messages you actually sent that got replies. This does more for quality than any amount of prompt instruction. Files: `config/examples.md` 2. Validate that the evidence was used Check for a distinctive token from the evidence in the draft. Regenerate once on failure, then hand it to a human. Files: `src/draft.js` 3. Read ten in a row If they blur together, the prompt is producing a template. Fix that before anything is sent to a real person. ### Done when - [ ] Each draft references its specific evidence, checked programmatically - [ ] A draft failing the check is flagged rather than sent - [ ] Every draft is within the length limit - [ ] Ten drafts read as ten distinct messages ### Watch out - A model given only a name and a company writes the same message every time, and recipients have seen it a thousand times. The evidence text is what makes the note worth reading, so make citing it a validated requirement rather than an instruction you hope was followed. ## M5 · A review queue with limits that bind Nothing is sent automatically, and the daily limit is enforced by the application rather than by your memory. ### Steps 1. Build the review queue Approve, edit or skip. Editing is the common case and should be as fast as approving. Files: `src/queue.js` 2. Enforce both caps in code A daily count and a rolling seven day count, both checked before an approval is allowed. Not a warning, a refusal. Files: `src/limits.js` 3. Prove there is no send path Grep the codebase for any outbound send. The absence is a feature and worth asserting in a test. Files: `test/no-send.test.js` ### Done when - [ ] No code path sends a message, asserted by a test - [ ] The daily cap blocks approvals and names the reset time - [ ] The rolling seven day cap is enforced separately - [ ] Every approval records who did it and when ### Watch out - The temptation at this point is to add a send button that posts through an automation interface, because everything else is built. That is the step that puts the account at risk, and the account is the asset. Keep the human in the loop and keep the caps conservative; limits vary by account age and acceptance rate, so the published ceiling is not your ceiling. ## M6 · Measuring whether it works (production only) You know your reply rate by signal type, and you stop watching the signals that do not pay. ### Steps 1. Record outcomes against drafts Sent, replied and whether the reply was positive. Manual entry is fine; the number matters more than the automation. Files: `src/outcomes.js` 2. Report by signal type The breakdown is the decision-making tool. Aggregate reply rate tells you nothing you can act on. Files: `src/report.js` 3. Write and test the data policy You are storing information about people who did not ask you to. Say what you keep, for how long, and remove someone on request. Then test the removal. Files: `docs/data-policy.md` ### Done when - [ ] Reply rate is reported per signal type over at least fifty messages - [ ] A watch has been retired based on the evidence - [ ] A removal request deletes that person's signals, scores and drafts - [ ] The data policy is written and matches what is actually stored ### Watch out - An aggregate reply rate hides that one signal type is carrying the whole result and three others are wasting your daily limit. Break it down by signal type from the first week, because you cannot reconstruct it later.
# Operations · Gojiberry AI ## Backup The SQLite file holds everything, including personal data about people who did not opt in. Back it up, and encrypt the backup, because the sensitivity is higher than the size suggests. ## Restore Restore the file and resume polling. Signals older than the cache window will re-enrich, which costs money, so a restore has a bill attached and it is worth knowing that in advance. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Poll failures per source, enrichment spend per week against a budget, and reply rate per signal type. A source that silently starts returning nothing looks identical to a quiet week unless you watch it. ## Incident checklist If a source's terms change, stop that watch first and decide afterwards. If someone asks to be removed, the deletion path from Phase 6 must cover signals, scores and drafts, not just the person row. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The test asserting no send path exists still passes - [ ] Both approval caps are enforced and tested by hitting them - [ ] Every draft cites its evidence, checked programmatically - [ ] A removal request is proved to delete signals, scores and drafts - [ ] The source terms note is current and dated ## Launch constraint Do not market omitted Gojiberry AI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. SQLite file holding watches, signals, people, scores and drafts. This is the backup. DB_PATH=./data/outreach.db # Required · secret. Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. ENRICHMENT_API_KEY=replace-with-provider-key # Required · secret. Your model provider console. Set a spend limit there as well. MODEL_API_KEY=sk-... # Required. Minimum score before enrichment is spent. Raising it cuts cost and coverage together. SCORE_THRESHOLD=70 # Required. How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. DAILY_APPROVAL_CAP=20 # Required. Rolling seven day cap, enforced separately from the daily one. Platform limits vary by account age and acceptance rate, so treat this as an upper bound rather than a target. WEEKLY_APPROVAL_CAP=100 # Required. How long an enrichment result stays fresh before a re-lookup is allowed. ENRICHMENT_CACHE_DAYS=90
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay to skip the assembly and the maintenance. Gojiberry turns a website URL into a running agent in ten minutes, keeps the scrapers working when a page layout changes, and puts the signal source, the enrichment waterfall, and both channels on one bill. Rent the parts yourself and the monthly cost drops, but you own every break, you are reconciling four dashboards, and the signals only stay useful if you keep feeding the watchlist new pages and creators.
xone enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner
xcross-customer benchmarking and the weekly self-tuning
xthe ten-minute setup: your version does not exist until you build it
xsomeone else absorbing the breakage when an actor or a LinkedIn endpoint changes
xa support line when a sending account gets restricted
Gojiberry AI pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| pro | $99/workspace | — | Email + socials; live in 5 minutes; numeric lead/message caps not published. |
free tierno free tier; free trial mentioned but no numeric caps published.
billingmonthly only; no annual plan verified
hidden costsNo numeric usage caps, seat limits or overages were published on the crawled page.
verified 2026-08-10 · source ↗
Vibecode Gojiberry AI
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Gojiberry AI replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Gojiberry AI cost?
Gojiberry AI costs about $99/month (Pro, checked 2026-08-03), which is $1188 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Gojiberry AI?
Honestly: one enrichment provider instead of a 15+ provider waterfall, so coverage on the hard contacts is thinner; cross-customer benchmarking and the weekly self-tuning; the ten-minute setup: your version does not exist until you build it; someone else absorbing the breakage when an actor or a LinkedIn endpoint changes; a support line when a sending account gets restricted. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Gojiberry AI?
Yes: Hermes Agent (self-hosted agent that can run the signal watch on a schedule instead of a cron service you write), n8n (self-hostable workflow automation, the usual no-code way to wire signals to outreach), Mautic (open-source marketing automation with contacts, campaigns, sequences, and suppression). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.