Build Gojiberry AI
YESreplaces $99/mosaves $1,188/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the watcher, the scorer and the queue.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - decidevaries 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 - API keytypically 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 - API keypay 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 - have readyfree
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 - decidefree
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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DB_PATH | required | ./data/outreach.db | SQLite file holding watches, signals, people, scores and drafts. This is the backup. |
ENRICHMENT_API_KEYsecret | required | replace-with-provider-key | Your enrichment provider's dashboard. Priced per lookup, so treat the key as a spending instrument. |
MODEL_API_KEYsecret | required | sk-... | Your model provider console. Set a spend limit there as well. |
SCORE_THRESHOLD | required | 70 | Minimum score before enrichment is spent. Raising it cuts cost and coverage together. |
DAILY_APPROVAL_CAP | required | 20 | How many drafts may be approved per day. Keep it well under platform ceilings, and lower for a new account. |
WEEKLY_APPROVAL_CAP | required | 100 | 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 | 90 | How long an enrichment result stays fresh before a re-lookup is allowed. |
The build, in order
One signal source, polled politely
Real signals land in your database from a source you are permitted to poll.
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.mdA 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.jsThe 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 · tick each as it passeswatch 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.
Scoring you can argue with
Each person gets a score and a readable list of reasons for it.
Positive signals, negative signals and hard disqualifiers, in a configuration file rather than in code.
Files
config/icp.jsonRules first. A model call only for what rules cannot express, and require it to return reasons.
Files
src/score.jsScore 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 · tick each as it passeswatch 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.
Enrichment, spent only where it counts
You pay for contact data only for people who already passed scoring.
One place in the pipeline where the threshold is applied. Easy to get wrong and expensive when it is.
Files
src/enrich.jsPeople change jobs, so a cached record has a shelf life. Record when it was fetched and re-enrich after that.
Files
src/enrich.jsCost 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 · tick each as it passeswatch 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.
Drafts that quote the signal
Each draft names the specific thing that prompted it, and reads like a person wrote it.
Two or three messages you actually sent that got replies. This does more for quality than any amount of prompt instruction.
Files
config/examples.mdCheck for a distinctive token from the evidence in the draft. Regenerate once on failure, then hand it to a human.
Files
src/draft.jsIf they blur together, the prompt is producing a template. Fix that before anything is sent to a real person.
done when · tick each as it passeswatch 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.
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.
Approve, edit or skip. Editing is the common case and should be as fast as approving.
Files
src/queue.jsA daily count and a rolling seven day count, both checked before an approval is allowed. Not a warning, a refusal.
Files
src/limits.jsGrep 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 · tick each as it passeswatch 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.
Measuring whether it worksproduct builder
You know your reply rate by signal type, and you stop watching the signals that do not pay.
Sent, replied and whether the reply was positive. Manual entry is fine; the number matters more than the automation.
Files
src/outcomes.jsThe breakdown is the decision-making tool. Aggregate reply rate tells you nothing you can act on.
Files
src/report.jsYou 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 · tick each as it passeswatch 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.
That is the whole plan for Gojiberry AI. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more sales outreach.