Build Dux-Soup
KINDAreplaces $14.99/mosaves $179.88/yrback to the verdict
An outreach assistant built around the constraint that you do the sending. It keeps the queue, drafts the message, records the outcome, and enforces the limits that stop follow-up becoming harassment, without ever touching a platform on your behalf.
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 local application.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree, needs memory
Why Drafts are built from your private notes about people, which is not material to send to a service by default.
Get it Any local runner with a model that follows instructions and writes plainly. open ↗
Verify
A draft generates locally from a set of notes - decidefree to read
Why The build deliberately does no automation, but you should know what the rules are, since the temptation to add it will arrive.
Get it Read the user agreement of every platform you will send through, particularly the sections on automated access and data extraction.
Verify
A written note states what each platform prohibits - have readyfree
Why Drafting quality and the refusal behaviour are only testable against real contacts where you either do or do not have specific context.
Get it People you have actually met or have a genuine reason to approach, with your real notes on each.
Verify
Fifty contacts exist, at least fifteen with no specific context beyond a job title - decidefree
Why They are constants in the code, and deciding them after using the tool means deciding them when you want more.
Get it Decide the maximum follow-ups per person, the minimum gap, and the weekly total, from the position of someone receiving them.
Verify
Three numbers are written down with the reasoning
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- contacts(id, name, org, role, source, added_at, notes) · source is how you actually met them - sequences(id, name, steps_json, max_touches, min_gap_days) - queue(id, contact_id, sequence_id, step, due_on, state, drafted_body, sent_at, skipped_reason) - outcomes(id, queue_id, kind, at, note) · kind is replied, declined, no_response or asked_to_stop - suppressions(contact_id, reason, at) · asked to stop is permanent and enforced before anything is queued - weekly_counts(week_start, sent) · your own volume, visible to you - There is deliberately no credentials table and no session store, because the build never acts on a platform.
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 | ~/.local/share/outreach/outreach.db | Contacts, notes and outcomes. Personal data about identified people. |
MAX_TOUCHES_PER_CONTACT | required | 3 | A constant in the queue. Not raisable at runtime. |
MIN_GAP_DAYS | required | 7 | Between touches to one person. Three in a week is pestering. |
WEEKLY_QUEUE_CAP | required | 20 | Total messages surfaced per week. Volume is what this build declines to provide. |
PLATFORM_AUTOMATION | required | none | Present as a constant so the answer is visible. There is no other value. |
DRAFT_MODEL | required | local | Drafts are built from private notes about people. |
The build, in order
A list of people you actually know of
Contacts arrive by your hand, with the reason you added them.
Answered, not defaulted. It is the question that keeps the list honest.
Files
src/contacts.tsThere is nothing to store because the build never signs in anywhere.
Files
src/schema.sqlAssert it in the dependency check, so it cannot creep back in.
Files
tests/no-automation.test.ts
done when · tick each as it passeswatch out- Adding an import path that accepts a scraped export makes the tool a consumer of automated collection even though it does none itself, and the obligations attach to holding the data rather than to gathering it. Require a source per contact and make the field one a person has to answer rather than a dropdown with a default.
Sequences with a ceiling
A follow-up sequence stops before it becomes pestering.
Configurable becomes nine. The ceiling should not depend on your mood.
Files
src/limits.tsThree messages in a week is not a sequence, it is pestering.
Files
src/queue.tsVolume is the thing this build declines to provide.
Files
src/limits.tssrc/limits.test.ts
done when · tick each as it passeswatch out- A configurable touch limit becomes six, then nine, because each individual increase seems reasonable and the person on the other end is not in the room. Make the ceiling a constant, and make the weekly cap one too, so the tool's restraint does not depend on your restraint.
Drafts that sound like you wrote them
A drafted message is specific enough to be worth sending.
A generic message is worse than none and this is where volume comes from.
Files
src/draft.tsSo you can see whether the draft has anything real in it.
Files
src/draft.tsYou will edit it. The record should be the truth.
Files
src/queue.ts
done when · tick each as it passeswatch out- A drafter that produces a plausible message from a name and a job title generates exactly the outreach everyone deletes, and it does so at whatever volume you allow. Require specific context, and refuse rather than producing a generic message, since the refusal is more useful than the draft.
You send it, and you say what happened
The record reflects reality because you entered it.
Not at send. A suppressed contact should never appear.
Files
src/queue.tstests/suppression.test.tsIncluding across re-import. It is not a preference.
Files
src/suppressions.tsThe record is honest because you made it.
Files
src/queue.ts
done when · tick each as it passeswatch out- Enforcing a suppression at send time means the person still appears in your queue every week, and one distracted click puts you back in their inbox after they asked you not to be. Check suppressions when queueing, so a suppressed contact never surfaces at all.
Seeing what your outreach is actually doingproduct builder
You can tell whether this is working or whether you are just sending things.
Asked-to-stop next to reply rate, same prominence.
Files
src/report.tsA rising complaint rate is the signal that matters.
Files
src/report.tsIncluding queue history, keeping only the suppression.
Files
src/deletion.ts
done when · tick each as it passeswatch out- Reporting only reply rate makes a sequence that annoys nine people and converts one look like a success, which is how outreach gets worse over time. Report the asked-to-stop rate beside it, with equal prominence, since it is the cost the reply rate is being bought with.
That is the whole plan for Dux-Soup. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Any automation of a platform account.
- Profile scraping and data extraction.
- Volume.
- Any claim that automated outreach is made compliant.
- Pacing heuristics and safety limits tuned across a lot of accounts, not guessed at by you
- Selector maintenance: when LinkedIn changes markup, someone else patches their extension and nobody patches yours
- Cloud execution, so campaigns keep running with your laptop shut
- Webhook and CRM plumbing into HubSpot, Pipedrive and friends, plus Zapier glue
- Campaign reporting, reply detection and sequence branching that actually stops when someone answers
- A reminder to follow up on people who replied, which is where outreach usually fails
- Notes captured from a conversation, since the context is what makes the next message worth sending
- A view of who you have not spoken to in a year, which is the useful half of a contact list
Need the files? The project pack on the verdict page hands your agent the whole brief · more sales outreach.