Recoup Sales Sweep
No email leaves this workflow before the user reviews and explicitly approves
that exact draft for sending. A request to act as CSO, run the sweep, contact a
quota, or "send outreach today" authorizes preparation, not unreviewed sends.
This applies to customers, cold prospects, replies, follow-ups, and test emails,
through every sender and tool. An available credential is not approval.
Read references/outreach-approval.md before preparing outreach. It defines the
quality review, the approval record, and the final check before a send. References,
subagents, scheduled jobs, and send helpers must follow the same boundary. Missing
or ambiguous approval means awaiting approval, never an inferred yes.
The repeatable prospecting + follow-up motion for Recoup staff. Eight data
pulls plus one synthesis: read every place a sales signal shows up, collapse it
into one ranked list of people to follow up with, then act — enrich/advance the
Attio pipeline and draft the outreach. First drafted 2026-07-23; refine the
steps below against each real run.
Define the window up front as UTC [FROM, TO) — usually since the last sweep
(default last 24h). Run the eight pulls in parallel (subagents work well; each is
self-contained), then synthesize.
Run the acquisition circuit-breaker check before anything else. If no new paid
customer has landed in 14 days, the default priority order will send you into
retention work again and the sweep will not produce a sale.
What this is (and what it is not)
This skill is forward-looking: it produces actions (who to contact, why,
how). It deliberately reuses the data plumbing of two sibling skills instead of
re-implementing it — load them for the ready-made queries/scripts:
recoup-internal-weekly-usage-review — the retrospective numbers (Stripe,
Privy logins, the credits/chats/tasks/valuations SQL pack). This sweep turns those
same pulls into follow-ups; that skill reports the totals.
recoup-internal-funnel-valuation-pipeline — the deep motion for one
valuation lead: Attio funnel recipes, qualification rubric, the valuation PDF,
and the outreach template. Step 3 hands qualified new valuations to it.
Also useful: recoup-internal-account-health-report (deep dive on one account
you're about to contact), recoup-internal-task-email-audit (task-run health
for step 6), recoup-platform-api-access (raw Recoup API).
Prerequisites
RECOUP_API_KEY / RECOUP_ACCESS_TOKEN — the primary source. A recoup_sk_
key, or an admin Privy JWT for cross-account work (see Source order). Load
recoup-platform-api-access for the endpoint map.
ATTIO_API_KEY — read + write (this sweep writes follow-ups back).
- Supabase read access to production (MCP
execute_sql or psql) — augmentation
only, for the fleet-wide aggregates the API has no endpoint for.
- Stripe: authenticated CLI or a live restricted key (pass
--api-key — the
keychain is often unreadable in sandboxed shells).
PRIVY_APP_ID + PRIVY_PROJECT_SECRET — Privy Management API.
EXA_API_KEY — cold outreach only (references/cold-outreach.md). It is the api
project's Vercel variable (one value across dev/preview/prod), so read it from
api/.env.local; the same key backs POST /api/research/people.
Research and drafting do not authorize sending. Attio follow-ups and confirmed-dead
task changes follow Guardrails; an email is a separate action requiring the exact-draft
approval in references/outreach-approval.md. Prepare the complete review packet before
asking. Do not call an endpoint that creates an account or triggers an email as a
research shortcut; check side effects before running a report, valuation, or task.
Source order — dogfood the API first
1. Recoup API → 2. Supabase. Reach for api.recoupable.dev first and drop to
SQL only for what it cannot answer. This is not a style preference: the sweep is
run by the people who own the product, and every pull is a free QA pass. The
2026-07-26 run found two product bugs purely by looking at API responses that no
SQL query would have surfaced.
Use the API for anything scoped to one account, artist, or task:
| Need |
Call |
Why not SQL |
| A customer's tasks |
GET /api/tasks?account_id= |
Returns owner_email, model, next_run, upcoming, recent_runs, trigger_schedule_id — none of which exist as columns on scheduled_actions. A schedule that will never fire is only visible here. |
| An artist's socials |
GET /api/artists?account_id= |
Socials are embedded as account_socials; the hand-rolled accounts → account_socials → socials join is where mistakes happen. |
| Remove an artist |
DELETE /api/artists/{id} |
Encodes the last-owner check and the fail-closed song-dependency guard. Raw SQL skips both. |
| Email → existing account |
Read-only account_emails lookup |
POST /api/accounts can create the account and email them; never use it for sales discovery. |
| Refresh a profile |
POST /api/socials/{id}/scrape |
Real scrape; SQL only shows you stale rows. |
Admin cross-account access. An admin Privy JWT plus an account_id override
lets you read/write in a customer's context without their key — account_id as a
query param on GETs, in the JSON body on PATCH/DELETE. Authorized by
validateAuthContext / checkIsAdmin. Confirm with GET /api/admins →
{"isAdmin":true}. These JWTs expire in ~1 hour — re-request rather than
debugging a sudden 401.
Drop to SQL only for the fleet-wide sweep — the cross-account aggregates that
have no endpoint: per-account credit burn, negative balances, went-silent accounts,
zombie tasks, valuation runs in a window, chat-title themes. That is most of the
eight pulls, and SQL is genuinely the right tool there. The rule is about
account-scoped reads, not about banning SQL.
When the two disagree, the API is the customer's truth — it is what the product
actually serves them. A DB row the API doesn't surface is not something the customer
can see.
Check this first — the acquisition circuit-breaker
Before running the sweep, ask one question: how many new paid customers have we
added in the last 14 days? If the answer is zero, the sweep's normal priority order
is actively wrong and you must switch modes.
Why this exists: seven of the eight pulls below read our own database. Every one
of them requires the person to already be a user. That makes this an excellent
farming motion with almost no hunting, and the default ranking (failed payment >
new customer > warm valuation lead) puts retention ahead of acquisition every single
time. On a small base with noisy retention signals, you never reach pull 3.
The one exception is the Agency Leads funnel (pull 4), which is the only surface in
this sweep where a live lead need never have touched the product. Agency revenue is a
separate line from subscriptions and it converts on a different clock: a build or
advisory engagement can close from a warm personal contact in a week, with no signup, no
credits and no activation. When the product funnel is cold, the agency board is often
the fastest path to revenue in the whole sweep — read it first, not last.
That failure mode is not hypothetical. Measured 2026-08-04: two months with zero new
sales, while 93 distinct accounts ran a catalog valuation in 60 days (241 runs in
June, 198 in July) and 56 of them ran exactly once and never returned. The demand
was there the whole time. Nobody worked it, because the sweep kept routing the day
into retention on existing accounts.
In acquisition mode, do this instead — in this order:
- Work the valuation backlog before anything else. Pull 3, but over the whole
backlog, not just the sweep window. Every account that ran a valuation and has no
Attio entry is an unworked, hands-raised lead. Prioritize one-and-done runs:
they got their number and left, which is the sharpest unanswered-question signal
we have.
- Work the Agency Leads board. Pull 4. Anything at
In Conversation with no time
on the calendar is a warm lead one message away from an advancement, and it does not
require the person to be a user at all.
- Look outside the database. No other pull does this, so it will not happen
unless you make it happen: named-account research, inbound-adjacent communities,
and the enterprise/custom-dev angle for any label or catalog-holder domain already
in Attio. Hunting is not a data pull; it is a decision to spend the day differently.
New agency leads land on
agency_leads at New. The step-by-step hunting
motion is references/cold-outreach.md: Exa Agent run → verify emails → research
the business → position across all services → earn one deliverable (valuation,
metadata run, YouTube gap audit, website proposal, superfan list) → short
one-link email. First run 2026-09-06 cost $1.27 and sent one email the same afternoon.
- Cap retention work. Timebox existing-account work so it cannot consume the day.
A zombie task or a stale follow-up is real, but it will never produce a new sale.
Return to the normal ordering once a new paid customer lands.
The eight pulls
Each pull is: what to read → the sales signal → the action. Always drop test
rows (sweetmantech*, sidney@, @example.com, [TEST], preview-auth-probe).
These eight are the fleet-wide layer, so most of them are legitimately SQL (or
Stripe/Privy/Attio). The API-first rule bites at the next step: the moment the
ranked list names a person, every drill-down on that account — their tasks, artists,
socials, scrape freshness — goes through api.recoupable.dev, not another SELECT.
Ranking is a SQL job; qualifying a named lead is an API job.
1. Stripe — new money to protect
- Read: subscriptions created / trials converted / first invoices in the
window; failed payments + disputes;
credits_topup sessions (weekly-usage-review
§3 has the exact calls).
- Signal: a new paid customer or trial-converter → welcome + make-sure-they-
activate follow-up (the highest-value moment to reach out). A failed payment /
dispute → save-the-account follow-up, today. A top-up loop (many identical
small sessions) → a "let's right-size your plan" conversation.
- Action: log the person in Attio; draft the welcome / recovery note.
⚠️ A raw checkout-session count measures bots, not demand. autoRechargeOrFail
mints a Checkout session server-side whenever a scheduled run is short of credits,
so the session list is dominated by cron artefacts with no human anywhere near them.
Measured 2026-08-04: 100 live sessions over two days, 0 paid, 0 with a
customer_email, 49% created inside four distinct minutes of the hour, across 22
accounts. Reading that as "people are trying to sign up" is backwards — and reading
its absence as "nobody wants us" is equally wrong.
Always filter before you quote a number: keep only sessions with a non-null
customer_email, then drop any minute-cluster that lines up with an account's cron
schedule. What survives is human intent. If nothing survives, say "we cannot see
human purchase intent" — not "there is none."
The same artefact appears at the customer level: a Stripe customer with
email: null, no payment methods, and a created timestamp inside the account's own
cron window was minted by the same code path. Never write that up as an "abandoned
checkout" — check created against the account's task schedule first.
2. Privy — who is signing in
- Read: the Privy-logins script from
recoup-internal-weekly-usage-review — it
paginates the Privy Management API into new signups vs returning for the window.
- Signal: a new signup with no chats yet → onboarding-stall follow-up. A
returning power user → expansion. An enterprise/label domain email → high-
value human outreach. Cross-check session-riders (long sessions never bump
latest_verified_at) against the interactive-chats query in step 7.
- Action: route new signups through the activation-funnel lens; flag warm
enterprise leads for a personal note.
3. Valuations — new catalog leads
- Read:
playcount_snapshots created in the window (weekly-usage-review SQL
query 13). A row with a NULL catalog is a measured-but-unclaimed run — a
brand-new warm lead nobody is working yet.
- Signal: every new valuation is a lead; unclaimed ones are the freshest.
- Action: hand each qualified lead to
recoup-internal-funnel-valuation-pipeline (research → qualify → enrich Attio
→ valuation PDF → outreach). Don't re-implement that motion here.
4. Attio — the follow-up backlog (the heart of this sweep)
The one pull that is only about follow-through. Read BOTH people funnels every
sweep — they are separate revenue lines and a sweep that reads one is blind to half
the pipeline:
| List |
api_slug |
What lives there |
| Valuation Leads |
valuation_leads |
Product/subscription funnel. Someone ran a catalog valuation and could become a Pro subscriber. |
| Agency Leads |
agency_leads |
Services funnel. Custom build, advisory, or retainer work. Usually arrives as a person emailing us, often from the personal network, and frequently has nothing to do with music. |
Load recoup-internal-funnel-valuation-pipeline for the Valuation Leads funnel
recipes (list entries, stages, field slugs, enrich/advance calls). The same Attio API
shapes work for agency_leads.
Never file a lead on the wrong list. An agency lead parked in Valuation Leads
inflates the valuation funnel with someone who never ran a valuation, exactly the way a
wrong lead_source does. If the person has no catalog interest, they are an agency lead.
A person can legitimately appear on both, but only once each side is real.
Valuation Leads — stages and SLA
New → Report Delivered → Qualified → Pro Offer Sent → Pro Active (Won) → Lost
- Owed a nudge: entries at Report Delivered or Pro Offer Sent whose
last activity is older than the follow-up SLA (default 3 business days) with
no reply/advance — someone we reached out to who went quiet.
Agency Leads — stages and SLA
New → In Conversation → Call Booked → Scoping → Proposal Sent → Won → Nurture → Lost
Fields: stage, buyer_or_referrer (Buyer / Referrer / Unknown), project_type
(Build / Advisory / Retainer / Unknown), owner, est_project_value, lost_reason.
- The stage that matters most is
In Conversation. It means they replied but no
time is on the calendar. Call Booked is deliberately separate, because "agreed to a
call" and "a time exists" are different states and the gap between them is where this
pipeline has historically died — two threads went cold for four years and three
and a half years respectively, both immediately after the lead said yes to a call.
Both lapses were ours. Flag any entry sitting at In Conversation for more than 3
days as the most urgent item on the agency side, ahead of anything further down the
board.
Nurture is where good contacts go to be forgotten unless policed. Any Nurture
entry with no dated follow-up task is a bug: create the task or move it to Lost.
- Resolve
buyer_or_referrer before scoping. Personal-network leads are frequently
introductions, not buyers, and treating a referrer as a buyer wastes a cycle.
- Leave
est_project_value empty until the work is scoped. A guessed number makes an
unqualified lead look qualified.
Both lists
- Flagged for follow-up: any entry whose note / next-step says to circle back.
- Signal → action:
- Non-responder, < 3 touches → draft the next nudge (new angle, not a resend).
- Non-responder, ≥ 3 touches over ~2 weeks → move to Lost with
lost_reason = No response so the board stays honest.
- Replied / booked → advance the stage and set the next step.
- Always set the
owner so every live lead is accountable to a person.
- A personal contact is not a sequence. Much of the agency list comes from the
founder's own network. One light nudge from a personal inbox with a new angle, never
an automated cadence.
5. Credits — engagement and billing risk
- Read: per-account burn, current balances, and prior-week-active-now-silent
(weekly-usage-review SQL queries 2, 4, 8).
- Signal: heavy burners → healthy; candidate for expansion / a testimonial.
Negative balance still burning → billing risk and a top-up conversation
(nothing stops them at zero). Went silent (active last week, zero this week) →
churn-save follow-up while it's still warm.
- ⚠️ Burn reads wrong in BOTH directions. High burn is not engagement — a daily
scheduled task produces usage events and looks like an active account. And near-zero
burn is not disengagement: it is just as often an account we never funded. Confirm
against step 7 before calling anyone active or dead.
- Read the balance and the plan together (see Credits mechanics below). An account
that has been at the same balance for months is not frugal, it is unrefilled.
- Action: log the follow-up; for a billing-risk account, pair the outreach with
the plan-fit conversation from step 1.
Before quoting anyone's balance, read references/credits-mechanics.md. The refill is
lazy and reading a balance mutates it; it SETS rather than adds, so on a free-tier
account above 333 a read silently reduces it; and isPro is derived from Stripe by
account id, so a customer whose Stripe record is missing metadata.accountId reads as
free tier while paying.
6. Tasks — zombie schedules on inactive accounts
Scheduled tasks keep firing (and burning AI tokens) long after an account goes
cold. Find enabled schedules whose owner has no recent usage and no recent
login — reach out to confirm the task still adds value, else turn it off to save
spend. scheduled_actions.last_run is dead (not written by the Workflows runner),
so judge activity by usage_events, not that column.
-- Zombie tasks: enabled schedules whose account had NO usage in the last 30 days.
-- (Cross-check task-run health with recoup-internal-task-email-audit.)
SELECT sa.account_id, ae.email, sa.title, sa.schedule, a2.name AS artist,
(SELECT MAX(created_at) FROM usage_events ue
WHERE ue.account_id = sa.account_id) AS last_usage
FROM scheduled_actions sa
LEFT JOIN LATERAL (SELECT email FROM account_emails
WHERE account_id = sa.account_id LIMIT 1) ae ON true
LEFT JOIN accounts a2 ON a2.id = sa.artist_account_id
WHERE sa.enabled = true
AND NOT EXISTS (SELECT 1 FROM usage_events ue
WHERE ue.account_id = sa.account_id
AND ue.created_at >= now() - interval '30 days')
ORDER BY ae.email NULLS LAST, sa.title;
- Signal: a still-firing task for a dormant / non-paying account = wasted
tokens and a re-engagement opening ("this report still runs — still useful?").
- Action: draft the check-in. Disable the schedule only after no-response or
an explicit "turn it off" — see Guardrails; disabling is a mutation.
7. Chats — what customers actually use Recoup for
- Read: chats created in the window with titles (weekly-usage-review SQL queries
5a/5b). Titles are admin-readable; bodies are owner-scoped (see account-health
caveat).
- ⚠️ Do not split on the
'Scheduled generation' topic — it no longer identifies
autopilot. Scheduled rooms now commonly carry a NULL topic, and titled rooms
include cron output (a real one: "Weekly Performance Dashboard 2026-07-20 to 2026-07-26"). Two tests that do work:
- Clock alignment. A cron fires at the same minute every day; a human is off-clock.
Group an account's rooms by
extract(minute from updated_at) — one dominant minute is
the schedule, everything else is a person.
- User messages per room (the sharper one). A scheduled run writes exactly one
user-role message per room, at the cron minute. A human session is multi-turn and
off-clock.
count(user_msgs) == count(rooms) for a month means nobody typed anything
that month.
- Signal: theme the titles (strategy / analytics / content / release) to see
what the product is used for — that shapes both the follow-up hook and product
feedback. A new interactive user is engaged and worth a check-in; a
recurring pain theme is a feature request plus a helpful-outreach opening.
- Action: note the usage themes; follow up with high-intent interactive users;
feed recurring friction back to product.
8. Outbound email — what we have already said to them
- Read: the Resend sent log (paginates well past a 14-day window) plus
email_send_log, with per-message delivery status.
- Signal: a lead with zero human outreach ever (only product email) → an
untouched first-contact opportunity. Bounces on a task email → a customer's
own report silently failing (fix before anyone sells them anything).
Duplicate sends of the same report → a runner bug hitting a real inbox — an
owned-and-fixed apology is the warmest opener there is.
- Action: never-contacted feeds the ranked list; bounces and duplicates become
fix-then-tell hooks (see The send loop).
Synthesis — one ranked follow-up list
- Collapse to people. Key every signal by account/email and merge across the
eight pulls — one person, all their signals, deduped.
- Rank by opportunity × urgency. Roughly: failed payment / churn-in-progress
(save today) > new paid customer (welcome now) > agency lead stalled at
In Conversation (a booked call is the cheapest advancement available) > warm
valuation or enterprise lead (work this week) > expansion / testimonial >
zombie-task cleanup.
This ordering assumes a funnel that is converting. If no new paid customer has
landed in 14 days, invert it — agency leads, warm valuation leads and enterprise
leads come first. See the acquisition circuit-breaker above.
- Correlate across sources — the cross-checks are where the real finds are:
- paid in Stripe but no Privy login → onboarding never landed; reach out now.
- valuation run with no Attio entry → an unworked lead; create it (step 3).
- negative credits and an enterprise domain → expansion priced as risk.
- zombie task on a went-silent account → one message does double duty
(re-engage + stop the token drain).
- agency lead at
In Conversation with no dated follow-up task → the exact
shape of every thread this pipeline has lost. Create the task before moving on.
- the same person on both funnels → decide which is real now and work that one;
pitching a subscription to someone who is talking to us about a build reads as a
sales sequence and costs the warmer thread.
- Act: write the follow-ups into Attio (create/advance entries, set
owner +
next step) and draft each outreach. Present the ranked list + drafts to the
operator for explicit draft approval. Do not count drafts as contacts or send
to fill a quota. Before drafting for any selected contact, build their
four-source dossier (next section) and review the actual correspondence.
The contact dossier — before any outreach is drafted
The moment the operator selects a specific contact (from the ranked list or on
their own), research that person into one concise four-row table and present it
alongside the draft. It is the pre-flight that catches "already paying",
"payment-blocked, not disinterested", "this IS the artist", and "we emailed
them yesterday". Build it fresh even if a sweep pull already touched the
account — the fleet aggregates hide the per-person timeline.
| Source |
What goes in the row |
The lookup that works |
| Privy |
first + last login events |
POST https://auth.privy.io/api/v1/users/email/address with {"address": "<email>"} (basic auth PRIVY_APP_ID:PRIVY_PROJECT_SECRET + privy-app-id header) → created_at and each linked account's first_verified_at / latest_verified_at. One call — no need to re-run the pagination script for one person. |
| Stripe |
customer / card / subscription status |
Find the customer by metadata.accountId (email search misses — see Tooling gotchas), then GET /v1/customers/{id}/payment_methods and GET /v1/subscriptions?customer=…&status=all. A customer with 0 payment methods and 0 subscriptions = they reached checkout and never paid — blocked, not disinterested. |
| Supabase |
credits_usage, sessions, chats |
All by account_id — except chats, which has no account_id column: join sessions.account_id → chats.session_id, then count chat_messages rows with role='user' per chat. Report typed chats separately from empty "New chat" rows — an untyped open is still a signal (they visited and bounced). Balance via SQL only, never the credits GET (see Credits mechanics). |
| Resend |
outbound emails we've sent them |
email_send_log by account_id (there are no to_email/subject columns; the subject is inside raw_body) → cross-ref each resend_id against the Resend API for subject + delivery status. Zero rows = we have never emailed this person from the product. |
Read the table before writing a word of outreach: the first/last-login pair
sets the "returning vs. new" frame, the Stripe row sets the ask (rescue vs.
upgrade vs. retention), the chats row tells you what they actually tried to do
in their own words (titles are admin-readable), and the Resend row is what
"we" have already said to them.
Product activity is not the conversation. Read the actual latest human inbound
and outbound emails, promises, meeting outcomes, and relationship owner before
choosing the ask. CRM summaries and delivery receipts cannot establish what the
customer wants now. If the thread is unavailable, flag that in the review packet;
do not silently assume no reply, an unanswered question, or permission to restart
an old proposal. Respect owner and no-contact restrictions when selecting contacts.
Then extend the dossier with four product-side tables — what the contact has
actually done with Recoup, in their own timeline:
| Table |
What goes in it |
The lookup that works |
| Lifetime activity |
Key dated events from signup to now, weighted to recent: first login, each typed chat (title verbatim), tasks created, valuation runs, checkout attempts, emails delivered or missed, last event. |
Merge the four source rows chronologically. A 10–15 row timeline reads faster than any aggregate and is where the story ("built the report Thursday, hit the paywall Monday") becomes visible. |
| Tasks |
Every task: title, artist, cadence, model, enabled, last run + status, next runs, does it email — and does the sent email match the intention the customer typed when they created it? |
GET /api/tasks?account_id= only (model / upcoming / recent_runs / owner_email don't exist on scheduled_actions). For intention-vs-output: compare the email_send_log.raw_body headline against the chat/task title that spawned it. A run that says COMPLETED with no matching email_send_log row delivered nothing — the customer thinks the task ran; it silently didn't reach them. |
| Artists |
Roster: each artist + every connected social (platform, handle/URL, followers, scrape freshness). |
GET /api/artists?account_id= — socials embedded as account_socials; the social's updated_at is the scrape date. A one-platform roster (e.g. Spotify only) is itself an outreach hook: nothing else is connected. |
| Valuation / catalog |
Run count + dates, claimed vs unclaimed, the value on file, what's in the catalog (songs / albums / total streams), and the crown jewels (top songs by plays). |
playcount_snapshots by account (the column is account, not account_id) → song_measurements.snapshot = ps.id, join songs ON songs.isrc = song_measurements.song; value is the play count. The dollar value is not in the DB (never persisted) — read it off the Attio auto-note or the valuation email. Repeat runs of the same catalog in one sitting = they're trying to answer a question; find out which one. |
The lead workspace — where every email lives
Each lead gets one folder in the operator's private workspace (workspace/sales/<lead>/,
its own git repo, never a product submodule). Two things in it are load-bearing for the
send loop; everything else (notes, PDFs, call transcripts, task prompts) sits beside them.
workspace/sales/<lead>/
EMAILS.md chronological index: every send + receipt, the read, the reply playbook
emails/
README.md the four lines below, so a cold reader knows the rule
sent/ verbatim final text of each send, one file, with its reply playbook
received/ verbatim inbound emails, one file each
threads/ verbatim multi-message Gmail threads (secrets redacted)
drafts/ every draft and revision; never the sent version
Files are YYYY-MM-DD-slug.md. Rules that keep the record trustworthy:
EMAILS.md is the index, emails/ is the evidence. The index entry links to the
file; the file holds the words. Never let a loose DRAFT-*.md sit at the folder root.
- Keep approval evidence beside the versioned draft. Record the user's exact
approval, its message reference or time, and the draft version it covers. Follow
references/outreach-approval.md; never manufacture an approval record from a quota.
- The sent file is the verified sent copy, not the draft. The send loop ends by
retrieving and comparing what actually went out before writing
emails/sent/;
saving a draft, scheduling a task, or making an Attio note does not make it sent.
- Every inbound reply gets a
received/ file the day it arrives, even a one-liner.
The 2026-08-25 reorg of two live accounts found one sent email that existed only in
Attio and two replies that existed only in Gmail; the reorg is what surfaced them.
- Secrets never enter the files. API tokens that ride inside task prompts get
redacted before the prompt or thread is copied in.
- Attio mirrors, it does not replace. Each send/receipt still becomes an Attio note
(the CRM's email sync misses bodies), and the note names the local file path.
The send loop — dossier to closed-out record
Run this sequence for every message: research → quality review → present exact
draft → await user approval → send only the approved version → verify and log.
Each message needs its own approval or inclusion in an explicitly approved batch
of unchanged versions. Approval of an unrelated previous send does not carry over.
Fix, then tell. The dossier almost always surfaces account defects —
duplicate artists or tasks, unconnected socials, blocked balances, schedules
that won't deliver. Repair only within the authorized scope via the API before
drafting; never trigger an unapproved email as part of a fix. Open with the specific
repair and its result, not a generic announcement that you reviewed the account. The
specific observed detail is what makes outreach read personal; a fix already
delivered is what makes it worth answering.
Draft in the house voice. Open with the concrete observation, never
meta-framing ("a quick note from a human" reads as automation). Offers are
things we will do, not commands to the reader ("I'll send you the link
next week, or now if you ask" beats "reply GO"). One plain close: "Let me
know how I can help." Valuation numbers always carry their caveats.
Every email carries at least one in-body link to a recoupable.dev
subdomain — deep-linked to the exact thing you are asking for, never just
the signature's home-page link. A reply that never becomes a site visit
cannot convert, and with no in-body link every action routes back through a
human by default, which is how a warm lead stalls. Match the link to the ask:
| The ask |
Link |
| Connect / fix streaming + social platforms |
https://chat.recoupable.dev/setup/socials |
| Re-run or check a catalog valuation |
https://chat.recoupable.dev/catalogs |
| Set up or edit a scheduled report |
https://chat.recoupable.dev/tasks |
| Anything else / general return visit |
https://chat.recoupable.dev |
Then run the draft through the unslop skill before presenting it.
Every customer-facing draft gets an unslop pass — it strips the AI tells
(em dashes, puffery, "not just X but Y", AI vocabulary) that make outreach
read machine-written, and its no-em-dash rule enforces this skill's own
standing rule for customer email. If unslop is not installed, get it from
skills.sh/cursor/plugins/unslop
(npx skills add — it installs at user level and applies to all writing).
The pass edits the draft in place; meaning and commitments must survive it
unchanged. Then apply the quality review in references/outreach-approval.md.
Style cleanup, a factual detail, and a product link alone do not make a useful email.
Present the complete draft and wait for approval. Show the recipient list,
sender/reply-to, subject, exact body including footer, links and attachments,
plus the reason to contact and any missing context or new commitments. Record
explicit user approval of that version. Do not send while the user is reviewing.
Send only the approved version; verify what went out. Immediately before
invoking any send tool, compare its complete payload with the approved draft.
Follow references/outreach-approval.md for changes, subsets, retries, and
missing approval. The operator can instead send personally; retrieve their
actual sent copy from the CRM/email provider or their paste and record any
changed commitments. Write verified text to emails/sent/ before closing out.
Close out every send the same way: complete the open task the send
fulfilled; write a sent-log note stating what was promised and the reply
playbook (what to do for each likely reply); do the keep-a-lead-warm
trio — set owner, log the note, create a dated follow-up task (a
Qualified enterprise lead once sat 21 days untouched because none of the
three existed), writing that task as a runbook — exact ids, API calls,
decision rules — executable by a cold reader; set the stage to what the
email actually did (a Pro pitch → Pro Offer Sent; delivering the number →
Report Delivered; a retention touch → no stage change).
On reply, prepare the playbook same-day. A reply does not approve our next
email. Research, do authorized work, and present the next draft for approval.
Log the reply; if the sender is not who the record says, correct the identity
immediately (rename, split
the people, fix future greetings); pay for substantive product feedback on
the spot (grant, verify the balance landed, say so in the reply);
reproduce any reported bug in a live browser the same day and file it with
evidence; and replace any task the reply mooted — a no-touch rule dies the
moment they engage, but the draft-approval requirement remains.
Meeting prep — the qualified-customer call
The send loop ends when a lead books a call. What happens next is a different
motion — not "who do we contact" but "what do we put in front of someone who has
already agreed to spend thirty minutes with us." Run it for any booked call with a
qualified customer. It produces exactly three artifacts: a pitch, a meeting
plan, and the PDFs to deliver.
Full workflow in references/meeting-prep.md — mining what the customer already
told us (read chat message bodies, never the auto-generated titles), finding
the gap between what they promise publicly and what their published process
delivers, the four document shapes, and the house format.
Two things that are easy to get wrong and expensive to undo:
- Never bring an account-health / account-status PDF to a prospect. That
format is internal tooling; run it on a quiet account and you hand someone an
audit of their colleague's inactivity.
- Data documents survive the call; argued documents often need re-aiming.
Invest in the measured ones, and argue only from what the customer already did,
never from an assumed goal.
Guardrails
- Keep preparation separate from delivery. Enriching/advancing Attio and
disabling a confirmed-dead task follow the existing limits; still confirm
before moving a lead to Lost or disabling a task. Sending an email additionally
requires explicit approval of that exact draft, through any tool or sender.
- Never mutate customer data with raw SQL — go through the API. An
UPDATE on
scheduled_actions writes the row but skips everything the endpoint does around
it: updateTask re-syncs the Trigger.dev schedule, and deleteArtist runs a
last-owner check plus a fail-closed song-dependency guard before hard-deleting.
A direct write leaves the row and the scheduler disagreeing, and the drift is
invisible in the table you just edited. Use PATCH/DELETE with an account_id
override. This applies to every customer-facing table, not just tasks.
- Draft first; no unapproved sends. Follow
references/outreach-approval.md
for every email. No quota, deadline, prior send, CRM stage, subagent recommendation,
or automatic goal continuation substitutes for the user's approval. Do not send
an apology or correction for an accidental send without review either.
- PII stays in the CRM. Names, emails, account IDs live in Attio/Supabase —
never paste them into shared docs or external services.
- Exclude test rows everywhere (
sweetmantech*, sidney@, @example.com,
[TEST], preview-auth-probe).
- Trust Stripe over the DB for paying status — the Supabase
subscriptions
mirror can be empty/stale.
Lessons from the first live run (2026-07-23)
Four leads worked end to end: an inbound valuation lead, a distributor/broker, a
major-label exec, and a dormant power user. What actually mattered.
Qualification
- Credit burn is not engagement. Two accounts looked "active today" and were
pure autopilot — the usage was a scheduled task firing. One had been dark 4 months
while its daily task quietly burned the balance negative. Check the last interactive
chat date, always (step 7 has the two tests that survive a NULL topic).
- And a quiet account is not a disengaged one. A subscriber of 19 months read as
dormant — 1 active day, 0 chats — because he had 456 credits against a 9,999
entitlement the whole time and nobody had ever read his balance to trigger the refill.
He had paid $405.46 for roughly 2% of one month's allowance. Fleet-wide, 1,499 of
1,646 accounts were carrying an unapplied refill and 133 were blocked at or below
zero. Before writing anyone off, check whether we ever funded them.
- Qualify the catalog before spending human time. A lead who ran a valuation
and asked "what can I sell for" held a 10-track public-domain classical
catalog worth ~$700 — no publishing to sell, since Bach and Chopin are public
domain. Capture works fine; qualification is the gap. Check repertoire type and
streams before drafting anything.
Valuation you can put in front of a buyer
- Say "asset value, not a bid." A catalog buyer asked "is this for the buy
…(truncated)
1---2name: recoup-internal-sales3description: INTERNAL Recoup staff sales workflow; invoke only for requests explicitly naming recoup-internal. Run the daily sales sweep across Stripe, Privy, valuations, both Attio funnels, credits, tasks, chats, and email history. Research and prioritize customers and cold prospects, maintain CRM follow-ups, and prepare outreach and meeting materials. Every email requires the user's explicit approval of the exact reviewed draft before sending. Use for sales sweeps, pipeline follow-up, cold outreach, new-lead research, and qualified-call prep. Requires Recoup API, Attio, Stripe, Privy and Supabase access; Exa for cold research. Never use for customer-facing or artist requests.4---5
6# Recoup Sales Sweep
7
8**No email leaves this workflow before the user reviews and explicitly approves
9that exact draft for sending.** A request to act as CSO, run the sweep, contact a
10quota, or "send outreach today" authorizes preparation, not unreviewed sends.
11This applies to customers, cold prospects, replies, follow-ups, and test emails,
12through every sender and tool. An available credential is not approval.
13
14Read `references/outreach-approval.md` before preparing outreach. It defines the
15quality review, the approval record, and the final check before a send. References,
16subagents, scheduled jobs, and send helpers must follow the same boundary. Missing
17or ambiguous approval means **awaiting approval**, never an inferred yes.
18
19The repeatable **prospecting + follow-up** motion for Recoup staff. Eight data
20pulls plus one synthesis: read every place a sales signal shows up, collapse it
21into one ranked list of people to follow up with, then act — enrich/advance the
22Attio pipeline and draft the outreach. First drafted 2026-07-23; refine the
23steps below against each real run.
24
25Define the window up front as UTC `[FROM, TO)` — usually **since the last sweep**
26(default last 24h). Run the eight pulls in parallel (subagents work well; each is
27self-contained), then synthesize.
28
29**Run the acquisition circuit-breaker check before anything else.** If no new paid
30customer has landed in 14 days, the default priority order will send you into
31retention work again and the sweep will not produce a sale.
32
33## What this is (and what it is not)
34
35This skill is **forward-looking**: it produces *actions* (who to contact, why,
36how). It deliberately reuses the data plumbing of two sibling skills instead of
37re-implementing it — load them for the ready-made queries/scripts:
38
39- **`recoup-internal-weekly-usage-review`** — the retrospective *numbers* (Stripe,
40 Privy logins, the credits/chats/tasks/valuations SQL pack). This sweep turns those
41 same pulls into follow-ups; that skill reports the totals.
42- **`recoup-internal-funnel-valuation-pipeline`** — the *deep* motion for one
43 valuation lead: Attio funnel recipes, qualification rubric, the valuation PDF,
44 and the outreach template. Step 3 hands qualified new valuations to it.
45
46Also useful: **`recoup-internal-account-health-report`** (deep dive on one account
47you're about to contact), **`recoup-internal-task-email-audit`** (task-run health
48for step 6), **`recoup-platform-api-access`** (raw Recoup API).
49
50## Prerequisites
51
52- `RECOUP_API_KEY` / `RECOUP_ACCESS_TOKEN` — **the primary source.** A `recoup_sk_`
53 key, or an admin Privy JWT for cross-account work (see Source order). Load
54 `recoup-platform-api-access` for the endpoint map.
55- `ATTIO_API_KEY` — read + write (this sweep *writes* follow-ups back).
56- Supabase read access to production (MCP `execute_sql` or psql) — **augmentation
57 only**, for the fleet-wide aggregates the API has no endpoint for.
58- Stripe: authenticated CLI **or** a live restricted key (pass `--api-key` — the
59 keychain is often unreadable in sandboxed shells).
60- `PRIVY_APP_ID` + `PRIVY_PROJECT_SECRET` — Privy Management API.
61- `EXA_API_KEY` — cold outreach only (`references/cold-outreach.md`). It is the api
62 project's Vercel variable (one value across dev/preview/prod), so read it from
63 `api/.env.local`; the same key backs `POST /api/research/people`.
64
65Research and drafting do not authorize sending. Attio follow-ups and confirmed-dead
66task changes follow Guardrails; an email is a separate action requiring the exact-draft
67approval in `references/outreach-approval.md`. Prepare the complete review packet before
68asking. Do not call an endpoint that creates an account or triggers an email as a
69research shortcut; check side effects before running a report, valuation, or task.
70
71## Source order — dogfood the API first
72
73**1. Recoup API → 2. Supabase.** Reach for `api.recoupable.dev` first and drop to
74SQL only for what it cannot answer. This is not a style preference: the sweep is
75run by the people who own the product, and every pull is a free QA pass. The
762026-07-26 run found two product bugs purely by looking at API responses that no
77SQL query would have surfaced.
78
79**Use the API for anything scoped to one account, artist, or task:**
80
81| Need | Call | Why not SQL |
82| --- | --- | --- |
83| A customer's tasks | `GET /api/tasks?account_id=` | Returns `owner_email`, `model`, `next_run`, `upcoming`, `recent_runs`, `trigger_schedule_id` — **none of which exist as columns** on `scheduled_actions`. A schedule that will never fire is only visible here. |
84| An artist's socials | `GET /api/artists?account_id=` | Socials are **embedded** as `account_socials`; the hand-rolled `accounts → account_socials → socials` join is where mistakes happen. |
85| Remove an artist | `DELETE /api/artists/{id}` | Encodes the last-owner check and the fail-closed song-dependency guard. Raw SQL skips both. |
86| Email → existing account | Read-only `account_emails` lookup | `POST /api/accounts` can create the account and email them; never use it for sales discovery. |
87| Refresh a profile | `POST /api/socials/{id}/scrape` | Real scrape; SQL only shows you stale rows. |
88
89**Admin cross-account access.** An admin Privy JWT plus an `account_id` override
90lets you read/write in a customer's context without their key — `account_id` as a
91**query param** on GETs, in the **JSON body** on `PATCH`/`DELETE`. Authorized by
92`validateAuthContext` / `checkIsAdmin`. Confirm with `GET /api/admins` →
93`{"isAdmin":true}`. These JWTs expire in **~1 hour** — re-request rather than
94debugging a sudden 401.
95
96**Drop to SQL only for the fleet-wide sweep** — the cross-account aggregates that
97have no endpoint: per-account credit burn, negative balances, went-silent accounts,
98zombie tasks, valuation runs in a window, chat-title themes. That is most of the
99eight pulls, and SQL is genuinely the right tool there. The rule is about
100*account-scoped* reads, not about banning SQL.
101
102**When the two disagree, the API is the customer's truth** — it is what the product
103actually serves them. A DB row the API doesn't surface is not something the customer
104can see.
105
106## Check this first — the acquisition circuit-breaker
107
108**Before running the sweep, ask one question: how many new paid customers have we
109added in the last 14 days?** If the answer is zero, the sweep's normal priority order
110is actively wrong and you must switch modes.
111
112Why this exists: **seven of the eight pulls below read our own database.** Every one
113of them requires the person to already be a user. That makes this an excellent
114*farming* motion with almost no *hunting*, and the default ranking (failed payment >
115new customer > warm valuation lead) puts retention ahead of acquisition every single
116time. On a small base with noisy retention signals, **you never reach pull 3.**
117
118**The one exception is the Agency Leads funnel** (pull 4), which is the only surface in
119this sweep where a live lead need never have touched the product. Agency revenue is a
120separate line from subscriptions and it converts on a different clock: a build or
121advisory engagement can close from a warm personal contact in a week, with no signup, no
122credits and no activation. When the product funnel is cold, **the agency board is often
123the fastest path to revenue in the whole sweep** — read it first, not last.
124
125That failure mode is not hypothetical. Measured 2026-08-04: two months with zero new
126sales, while **93 distinct accounts ran a catalog valuation in 60 days** (241 runs in
127June, 198 in July) and **56 of them ran exactly once and never returned.** The demand
128was there the whole time. Nobody worked it, because the sweep kept routing the day
129into retention on existing accounts.
130
131**In acquisition mode, do this instead — in this order:**
132
1331. **Work the valuation backlog before anything else.** Pull 3, but over the *whole
134 backlog*, not just the sweep window. Every account that ran a valuation and has no
135 Attio entry is an unworked, hands-raised lead. Prioritize **one-and-done runs**:
136 they got their number and left, which is the sharpest unanswered-question signal
137 we have.
1382. **Work the Agency Leads board.** Pull 4. Anything at `In Conversation` with no time
139 on the calendar is a warm lead one message away from an advancement, and it does not
140 require the person to be a user at all.
1413. **Look outside the database.** No other pull does this, so it will not happen
142 unless you make it happen: named-account research, inbound-adjacent communities,
143 and the enterprise/custom-dev angle for any label or catalog-holder domain already
144 in Attio. Hunting is not a data pull; it is a decision to spend the day differently.
145 New agency leads land on `agency_leads` at **New**. **The step-by-step hunting
146 motion is `references/cold-outreach.md`**: Exa Agent run → verify emails → research
147 the business → position across all services → earn one deliverable (valuation,
148 metadata run, YouTube gap audit, website proposal, superfan list) → short
149 one-link email. First run 2026-09-06 cost $1.27 and sent one email the same afternoon.
1504. **Cap retention work.** Timebox existing-account work so it cannot consume the day.
151 A zombie task or a stale follow-up is real, but it will never produce a new sale.
152
153Return to the normal ordering once a new paid customer lands.
154
155## The eight pulls
156
157Each pull is: **what to read → the sales signal → the action**. Always drop test
158rows (`sweetmantech*`, `sidney@`, `@example.com`, `[TEST]`, `preview-auth-probe`).
159
160These eight are the *fleet-wide* layer, so most of them are legitimately SQL (or
161Stripe/Privy/Attio). The API-first rule bites at the **next** step: the moment the
162ranked list names a person, every drill-down on that account — their tasks, artists,
163socials, scrape freshness — goes through `api.recoupable.dev`, not another SELECT.
164Ranking is a SQL job; qualifying a named lead is an API job.
165
166### 1. Stripe — new money to protect
167
168- **Read:** subscriptions created / trials converted / first invoices in the
169 window; failed payments + disputes; `credits_topup` sessions (weekly-usage-review
170 §3 has the exact calls).
171- **Signal:** a **new paid customer or trial-converter** → welcome + make-sure-they-
172 activate follow-up (the highest-value moment to reach out). A **failed payment /
173 dispute** → save-the-account follow-up, today. A **top-up loop** (many identical
174 small sessions) → a "let's right-size your plan" conversation.
175- **Action:** log the person in Attio; draft the welcome / recovery note.
176
177> ⚠️ **A raw checkout-session count measures bots, not demand.** `autoRechargeOrFail`
178> mints a Checkout session server-side whenever a scheduled run is short of credits,
179> so the session list is dominated by cron artefacts with no human anywhere near them.
180> Measured 2026-08-04: **100 live sessions over two days, 0 paid, 0 with a
181> `customer_email`, 49% created inside four distinct minutes of the hour, across 22
182> accounts.** Reading that as "people are trying to sign up" is backwards — and reading
183> its absence as "nobody wants us" is equally wrong.
184>
185> **Always filter before you quote a number:** keep only sessions with a non-null
186> `customer_email`, then drop any minute-cluster that lines up with an account's cron
187> schedule. What survives is human intent. If nothing survives, say **"we cannot see
188> human purchase intent"** — not "there is none."
189>
190> The same artefact appears at the *customer* level: a Stripe customer with
191> `email: null`, no payment methods, and a `created` timestamp inside the account's own
192> cron window was minted by the same code path. Never write that up as an "abandoned
193> checkout" — check `created` against the account's task schedule first.
194
195### 2. Privy — who is signing in
196
197- **Read:** the Privy-logins script from `recoup-internal-weekly-usage-review` — it
198 paginates the Privy Management API into new signups vs returning for the window.
199- **Signal:** a **new signup with no chats yet** → onboarding-stall follow-up. A
200 **returning power user** → expansion. An **enterprise/label domain** email → high-
201 value human outreach. Cross-check session-riders (long sessions never bump
202 `latest_verified_at`) against the interactive-chats query in step 7.
203- **Action:** route new signups through the activation-funnel lens; flag warm
204 enterprise leads for a personal note.
205
206### 3. Valuations — new catalog leads
207
208- **Read:** `playcount_snapshots` created in the window (weekly-usage-review SQL
209 query 13). A row with a **NULL `catalog`** is a *measured-but-unclaimed* run — a
210 brand-new warm lead nobody is working yet.
211- **Signal:** every new valuation is a lead; unclaimed ones are the freshest.
212- **Action:** hand each qualified lead to
213 **`recoup-internal-funnel-valuation-pipeline`** (research → qualify → enrich Attio
214 → valuation PDF → outreach). Don't re-implement that motion here.
215
216### 4. Attio — the follow-up backlog (the heart of this sweep)
217
218The one pull that is *only* about follow-through. **Read BOTH people funnels every
219sweep** — they are separate revenue lines and a sweep that reads one is blind to half
220the pipeline:
221
222| List | `api_slug` | What lives there |
223| --- | --- | --- |
224| **Valuation Leads** | `valuation_leads` | Product/subscription funnel. Someone ran a catalog valuation and could become a Pro subscriber. |
225| **Agency Leads** | `agency_leads` | Services funnel. Custom build, advisory, or retainer work. Usually arrives as a person emailing us, often from the personal network, and frequently has nothing to do with music. |
226
227Load **`recoup-internal-funnel-valuation-pipeline`** for the Valuation Leads funnel
228recipes (list entries, stages, field slugs, enrich/advance calls). The same Attio API
229shapes work for `agency_leads`.
230
231**Never file a lead on the wrong list.** An agency lead parked in Valuation Leads
232inflates the valuation funnel with someone who never ran a valuation, exactly the way a
233wrong `lead_source` does. If the person has no catalog interest, they are an agency lead.
234A person can legitimately appear on both, but only once each side is real.
235
236#### Valuation Leads — stages and SLA
237
238`New → Report Delivered → Qualified → Pro Offer Sent → Pro Active (Won) → Lost`
239
240- **Owed a nudge:** entries at **Report Delivered** or **Pro Offer Sent** whose
241 last activity is older than the follow-up SLA (default **3 business days**) with
242 no reply/advance — someone we reached out to who went quiet.
243
244#### Agency Leads — stages and SLA
245
246`New → In Conversation → Call Booked → Scoping → Proposal Sent → Won → Nurture → Lost`
247
248Fields: `stage`, `buyer_or_referrer` (Buyer / Referrer / Unknown), `project_type`
249(Build / Advisory / Retainer / Unknown), `owner`, `est_project_value`, `lost_reason`.
250
251- **The stage that matters most is `In Conversation`.** It means they replied but **no
252 time is on the calendar**. `Call Booked` is deliberately separate, because "agreed to a
253 call" and "a time exists" are different states and the gap between them is where this
254 pipeline has historically died — two threads went cold for **four years** and **three
255 and a half years** respectively, both immediately after the lead said yes to a call.
256 Both lapses were ours. **Flag any entry sitting at `In Conversation` for more than 3
257 days as the most urgent item on the agency side**, ahead of anything further down the
258 board.
259- **`Nurture` is where good contacts go to be forgotten** unless policed. Any Nurture
260 entry with no dated follow-up task is a bug: create the task or move it to **Lost**.
261- **Resolve `buyer_or_referrer` before scoping.** Personal-network leads are frequently
262 introductions, not buyers, and treating a referrer as a buyer wastes a cycle.
263- **Leave `est_project_value` empty until the work is scoped.** A guessed number makes an
264 unqualified lead look qualified.
265
266#### Both lists
267
268- **Flagged for follow-up:** any entry whose note / next-step says to circle back.
269- **Signal → action:**
270 - Non-responder, < 3 touches → draft the next nudge (new angle, not a resend).
271 - Non-responder, ≥ 3 touches over ~2 weeks → move to **Lost** with
272 `lost_reason = No response` so the board stays honest.
273 - Replied / booked → advance the stage and set the next step.
274- Always set the `owner` so every live lead is accountable to a person.
275- **A personal contact is not a sequence.** Much of the agency list comes from the
276 founder's own network. One light nudge from a personal inbox with a new angle, never
277 an automated cadence.
278
279### 5. Credits — engagement and billing risk
280
281- **Read:** per-account burn, current balances, and prior-week-active-now-silent
282 (weekly-usage-review SQL queries 2, 4, 8).
283- **Signal:** **heavy burners** → healthy; candidate for expansion / a testimonial.
284 **Negative balance still burning** → billing risk *and* a top-up conversation
285 (nothing stops them at zero). **Went silent** (active last week, zero this week) →
286 churn-save follow-up while it's still warm.
287- ⚠️ **Burn reads wrong in BOTH directions.** High burn is not engagement — a daily
288 scheduled task produces usage events and looks like an active account. And **near-zero
289 burn is not disengagement**: it is just as often an account we never funded. Confirm
290 against step 7 before calling anyone active *or* dead.
291- **Read the balance and the plan together** (see *Credits mechanics* below). An account
292 that has been at the same balance for months is not frugal, it is unrefilled.
293- **Action:** log the follow-up; for a billing-risk account, pair the outreach with
294 the plan-fit conversation from step 1.
295
296**Before quoting anyone's balance, read `references/credits-mechanics.md`.** The refill is
297lazy and reading a balance **mutates** it; it SETS rather than adds, so on a free-tier
298account above 333 a read silently *reduces* it; and `isPro` is derived from Stripe by
299account id, so a customer whose Stripe record is missing `metadata.accountId` reads as
300free tier while paying.
301
302### 6. Tasks — zombie schedules on inactive accounts
303
304Scheduled tasks keep firing (and burning AI tokens) long after an account goes
305cold. Find enabled schedules whose owner has **no recent usage and no recent
306login** — reach out to confirm the task still adds value, else turn it off to save
307spend. `scheduled_actions.last_run` is dead (not written by the Workflows runner),
308so judge activity by `usage_events`, not that column.
309
310```sql
311-- Zombie tasks: enabled schedules whose account had NO usage in the last 30 days.
312-- (Cross-check task-run health with recoup-internal-task-email-audit.)
313SELECT sa.account_id, ae.email, sa.title, sa.schedule, a2.name AS artist,
314 (SELECT MAX(created_at) FROM usage_events ue
315 WHERE ue.account_id = sa.account_id) AS last_usage
316FROM scheduled_actions sa
317LEFT JOIN LATERAL (SELECT email FROM account_emails
318 WHERE account_id = sa.account_id LIMIT 1) ae ON true
319LEFT JOIN accounts a2 ON a2.id = sa.artist_account_id
320WHERE sa.enabled = true
321 AND NOT EXISTS (SELECT 1 FROM usage_events ue
322 WHERE ue.account_id = sa.account_id
323 AND ue.created_at >= now() - interval '30 days')
324ORDER BY ae.email NULLS LAST, sa.title;
325```
326
327- **Signal:** a still-firing task for a dormant / non-paying account = wasted
328 tokens **and** a re-engagement opening ("this report still runs — still useful?").
329- **Action:** draft the check-in. Disable the schedule **only** after no-response or
330 an explicit "turn it off" — see Guardrails; disabling is a mutation.
331
332### 7. Chats — what customers actually use Recoup for
333
334- **Read:** chats created in the window with titles (weekly-usage-review SQL queries
335 5a/5b). Titles are admin-readable; **bodies are owner-scoped** (see account-health
336 caveat).
337- ⚠️ **Do not split on the `'Scheduled generation'` topic — it no longer identifies
338 autopilot.** Scheduled rooms now commonly carry a **NULL topic**, and titled rooms
339 include cron output (a real one: `"Weekly Performance Dashboard 2026-07-20 to
340 2026-07-26"`). Two tests that do work:
341 - **Clock alignment.** A cron fires at the same minute every day; a human is off-clock.
342 Group an account's rooms by `extract(minute from updated_at)` — one dominant minute is
343 the schedule, everything else is a person.
344 - **User messages per room** (the sharper one). A scheduled run writes **exactly one
345 user-role message per room**, at the cron minute. A human session is multi-turn and
346 off-clock. `count(user_msgs) == count(rooms)` for a month means nobody typed anything
347 that month.
348- **Signal:** theme the titles (strategy / analytics / content / release) to see
349 *what* the product is used for — that shapes both the follow-up hook and product
350 feedback. A **new interactive user** is engaged and worth a check-in; a
351 **recurring pain theme** is a feature request plus a helpful-outreach opening.
352- **Action:** note the usage themes; follow up with high-intent interactive users;
353 feed recurring friction back to product.
354
355### 8. Outbound email — what we have already said to them
356
357- **Read:** the Resend sent log (paginates well past a 14-day window) plus
358 `email_send_log`, with per-message delivery status.
359- **Signal:** a lead with **zero human outreach ever** (only product email) → an
360 untouched first-contact opportunity. **Bounces on a task email** → a customer's
361 own report silently failing (fix before anyone sells them anything).
362 **Duplicate sends of the same report** → a runner bug hitting a real inbox — an
363 owned-and-fixed apology is the warmest opener there is.
364- **Action:** never-contacted feeds the ranked list; bounces and duplicates become
365 fix-then-tell hooks (see The send loop).
366
367## Synthesis — one ranked follow-up list
368
3691. **Collapse to people.** Key every signal by account/email and merge across the
370 eight pulls — one person, all their signals, deduped.
3712. **Rank by opportunity × urgency.** Roughly: failed payment / churn-in-progress
372 (save today) > new paid customer (welcome now) > **agency lead stalled at
373 `In Conversation`** (a booked call is the cheapest advancement available) > warm
374 valuation or enterprise lead (work this week) > expansion / testimonial >
375 zombie-task cleanup.
376 **This ordering assumes a funnel that is converting.** If no new paid customer has
377 landed in 14 days, invert it — agency leads, warm valuation leads and enterprise
378 leads come first. See the acquisition circuit-breaker above.
3793. **Correlate across sources** — the cross-checks are where the real finds are:
380 - paid in Stripe **but** no Privy login → onboarding never landed; reach out now.
381 - valuation run **with no Attio entry** → an unworked lead; create it (step 3).
382 - negative credits **and** an enterprise domain → expansion priced as risk.
383 - zombie task **on** a went-silent account → one message does double duty
384 (re-engage + stop the token drain).
385 - agency lead at **`In Conversation`** with **no dated follow-up task** → the exact
386 shape of every thread this pipeline has lost. Create the task before moving on.
387 - the same person on **both** funnels → decide which is real *now* and work that one;
388 pitching a subscription to someone who is talking to us about a build reads as a
389 sales sequence and costs the warmer thread.
3904. **Act:** write the follow-ups into Attio (create/advance entries, set `owner` +
391 next step) and draft each outreach. Present the ranked list + drafts to the
392 operator for explicit draft approval. Do not count drafts as contacts or send
393 to fill a quota. Before drafting for any selected contact, build their
394 four-source dossier (next section) and review the actual correspondence.
395
396## The contact dossier — before any outreach is drafted
397
398The moment the operator selects a specific contact (from the ranked list or on
399their own), research that person into one concise four-row table and present it
400alongside the draft. It is the pre-flight that catches "already paying",
401"payment-blocked, not disinterested", "this IS the artist", and "we emailed
402them yesterday". Build it fresh even if a sweep pull already touched the
403account — the fleet aggregates hide the per-person timeline.
404
405| Source | What goes in the row | The lookup that works |
406| --- | --- | --- |
407| **Privy** | first + last login events | `POST https://auth.privy.io/api/v1/users/email/address` with `{"address": "<email>"}` (basic auth `PRIVY_APP_ID:PRIVY_PROJECT_SECRET` + `privy-app-id` header) → `created_at` and each linked account's `first_verified_at` / `latest_verified_at`. One call — no need to re-run the pagination script for one person. |
408| **Stripe** | customer / card / subscription status | Find the customer by `metadata.accountId` (email search misses — see Tooling gotchas), then `GET /v1/customers/{id}/payment_methods` and `GET /v1/subscriptions?customer=…&status=all`. A customer with 0 payment methods and 0 subscriptions = they reached checkout and never paid — blocked, not disinterested. |
409| **Supabase** | credits_usage, sessions, chats | All by `account_id` — except `chats`, which has **no `account_id` column**: join `sessions.account_id → chats.session_id`, then count `chat_messages` rows with `role='user'` per chat. Report typed chats separately from empty `"New chat"` rows — an untyped open is still a signal (they visited and bounced). Balance via SQL only, never the credits GET (see Credits mechanics). |
410| **Resend** | outbound emails we've sent them | `email_send_log` by `account_id` (there are no `to_email`/`subject` columns; the subject is inside `raw_body`) → cross-ref each `resend_id` against the Resend API for subject + delivery status. Zero rows = we have never emailed this person from the product. |
411
412Read the table before writing a word of outreach: the first/last-login pair
413sets the "returning vs. new" frame, the Stripe row sets the ask (rescue vs.
414upgrade vs. retention), the chats row tells you what they actually tried to do
415in their own words (titles are admin-readable), and the Resend row is what
416"we" have already said to them.
417
418**Product activity is not the conversation.** Read the actual latest human inbound
419and outbound emails, promises, meeting outcomes, and relationship owner before
420choosing the ask. CRM summaries and delivery receipts cannot establish what the
421customer wants now. If the thread is unavailable, flag that in the review packet;
422do not silently assume no reply, an unanswered question, or permission to restart
423an old proposal. Respect owner and no-contact restrictions when selecting contacts.
424
425Then extend the dossier with four product-side tables — what the contact has
426actually *done* with Recoup, in their own timeline:
427
428| Table | What goes in it | The lookup that works |
429| --- | --- | --- |
430| **Lifetime activity** | Key dated events from signup to now, weighted to recent: first login, each typed chat (title verbatim), tasks created, valuation runs, checkout attempts, emails delivered or missed, last event. | Merge the four source rows chronologically. A 10–15 row timeline reads faster than any aggregate and is where the story ("built the report Thursday, hit the paywall Monday") becomes visible. |
431| **Tasks** | Every task: title, artist, cadence, model, enabled, last run + status, next runs, does it email — and does the sent email match the intention the customer typed when they created it? | `GET /api/tasks?account_id=` only (model / `upcoming` / `recent_runs` / `owner_email` don't exist on `scheduled_actions`). For intention-vs-output: compare the `email_send_log.raw_body` headline against the chat/task title that spawned it. A run that says COMPLETED with **no matching `email_send_log` row** delivered nothing — the customer thinks the task ran; it silently didn't reach them. |
432| **Artists** | Roster: each artist + every connected social (platform, handle/URL, followers, scrape freshness). | `GET /api/artists?account_id=` — socials embedded as `account_socials`; the social's `updated_at` is the scrape date. A one-platform roster (e.g. Spotify only) is itself an outreach hook: nothing else is connected. |
433| **Valuation / catalog** | Run count + dates, claimed vs unclaimed, the value on file, what's in the catalog (songs / albums / total streams), and the crown jewels (top songs by plays). | `playcount_snapshots` by `account` (the column is `account`, not `account_id`) → `song_measurements.snapshot = ps.id`, join `songs ON songs.isrc = song_measurements.song`; `value` is the play count. The dollar value is **not** in the DB (never persisted) — read it off the Attio auto-note or the valuation email. Repeat runs of the same catalog in one sitting = they're trying to answer a question; find out which one. |
434
435## The lead workspace — where every email lives
436
437Each lead gets one folder in the operator's private workspace (`workspace/sales/<lead>/`,
438its own git repo, never a product submodule). Two things in it are load-bearing for the
439send loop; everything else (notes, PDFs, call transcripts, task prompts) sits beside them.
440
441```
442workspace/sales/<lead>/
443 EMAILS.md chronological index: every send + receipt, the read, the reply playbook
444 emails/
445 README.md the four lines below, so a cold reader knows the rule
446 sent/ verbatim final text of each send, one file, with its reply playbook
447 received/ verbatim inbound emails, one file each
448 threads/ verbatim multi-message Gmail threads (secrets redacted)
449 drafts/ every draft and revision; never the sent version
450```
451
452Files are `YYYY-MM-DD-slug.md`. Rules that keep the record trustworthy:
453
454- **`EMAILS.md` is the index, `emails/` is the evidence.** The index entry links to the
455 file; the file holds the words. Never let a loose `DRAFT-*.md` sit at the folder root.
456- **Keep approval evidence beside the versioned draft.** Record the user's exact
457 approval, its message reference or time, and the draft version it covers. Follow
458 `references/outreach-approval.md`; never manufacture an approval record from a quota.
459- **The sent file is the verified sent copy, not the draft.** The send loop ends by
460 retrieving and comparing what actually went out before writing `emails/sent/`;
461 saving a draft, scheduling a task, or making an Attio note does not make it sent.
462- **Every inbound reply gets a `received/` file the day it arrives**, even a one-liner.
463 The 2026-08-25 reorg of two live accounts found one sent email that existed only in
464 Attio and two replies that existed only in Gmail; the reorg is what surfaced them.
465- **Secrets never enter the files.** API tokens that ride inside task prompts get
466 redacted before the prompt or thread is copied in.
467- **Attio mirrors, it does not replace.** Each send/receipt still becomes an Attio note
468 (the CRM's email sync misses bodies), and the note names the local file path.
469
470## The send loop — dossier to closed-out record
471
472Run this sequence for every message: research → quality review → present exact
473draft → await user approval → send only the approved version → verify and log.
474Each message needs its own approval or inclusion in an explicitly approved batch
475of unchanged versions. Approval of an unrelated previous send does not carry over.
476
4771. **Fix, then tell.** The dossier almost always surfaces account defects —
478 duplicate artists or tasks, unconnected socials, blocked balances, schedules
479 that won't deliver. Repair only within the authorized scope via the API *before*
480 drafting; never trigger an unapproved email as part of a fix. Open with the specific
481 repair and its result, not a generic announcement that you reviewed the account. The
482 specific observed detail is what makes outreach read personal; a fix already
483 delivered is what makes it worth answering.
4842. **Draft in the house voice.** Open with the concrete observation, never
485 meta-framing ("a quick note from a human" reads as automation). Offers are
486 things *we* will do, not commands to the reader ("I'll send you the link
487 next week, or now if you ask" beats "reply GO"). One plain close: "Let me
488 know how I can help." Valuation numbers always carry their caveats.
489 **Every email carries at least one in-body link to a `recoupable.dev`
490 subdomain** — deep-linked to the exact thing you are asking for, never just
491 the signature's home-page link. A reply that never becomes a site visit
492 cannot convert, and with no in-body link every action routes back through a
493 human by default, which is how a warm lead stalls. Match the link to the ask:
494
495 | The ask | Link |
496 | --- | --- |
497 | Connect / fix streaming + social platforms | `https://chat.recoupable.dev/setup/socials` |
498 | Re-run or check a catalog valuation | `https://chat.recoupable.dev/catalogs` |
499 | Set up or edit a scheduled report | `https://chat.recoupable.dev/tasks` |
500 | Anything else / general return visit | `https://chat.recoupable.dev` |
501
502 **Then run the draft through the `unslop` skill before presenting it.**
503 Every customer-facing draft gets an unslop pass — it strips the AI tells
504 (em dashes, puffery, "not just X but Y", AI vocabulary) that make outreach
505 read machine-written, and its no-em-dash rule enforces this skill's own
506 standing rule for customer email. If `unslop` is not installed, get it from
507 [skills.sh/cursor/plugins/unslop](https://www.skills.sh/cursor/plugins/unslop)
508 (`npx skills add` — it installs at user level and applies to all writing).
509 The pass edits the draft in place; meaning and commitments must survive it
510 unchanged. Then apply the quality review in `references/outreach-approval.md`.
511 Style cleanup, a factual detail, and a product link alone do not make a useful email.
5123. **Present the complete draft and wait for approval.** Show the recipient list,
513 sender/reply-to, subject, exact body including footer, links and attachments,
514 plus the reason to contact and any missing context or new commitments. Record
515 explicit user approval of that version. Do not send while the user is reviewing.
5164. **Send only the approved version; verify what went out.** Immediately before
517 invoking any send tool, compare its complete payload with the approved draft.
518 Follow `references/outreach-approval.md` for changes, subsets, retries, and
519 missing approval. The operator can instead send personally; retrieve their
520 actual sent copy from the CRM/email provider or their paste and record any
521 changed commitments. Write verified text to `emails/sent/` before closing out.
5225. **Close out every send the same way:** complete the open task the send
523 fulfilled; write a sent-log note stating what was promised **and the reply
524 playbook** (what to do for each likely reply); do the **keep-a-lead-warm
525 trio — set `owner`, log the note, create a dated follow-up task** (a
526 Qualified enterprise lead once sat 21 days untouched because none of the
527 three existed), writing that task as a **runbook** — exact ids, API calls,
528 decision rules — executable by a cold reader; set the stage to what the
529 email actually did (a Pro pitch → Pro Offer Sent; delivering the number →
530 Report Delivered; a retention touch → no stage change).
5316. **On reply, prepare the playbook same-day.** A reply does not approve our next
532 email. Research, do authorized work, and present the next draft for approval.
533 Log the reply; if the sender is not who the record says, correct the identity
534 immediately (rename, split
535 the people, fix future greetings); pay for substantive product feedback on
536 the spot (grant, verify the balance landed, say so in the reply);
537 reproduce any reported bug in a live browser the same day and file it with
538 evidence; and replace any task the reply mooted — a no-touch rule dies the
539 moment they engage, but the draft-approval requirement remains.
540
541## Meeting prep — the qualified-customer call
542
543The send loop ends when a lead books a call. What happens next is a different
544motion — not "who do we contact" but "what do we put in front of someone who has
545already agreed to spend thirty minutes with us." Run it for any booked call with a
546qualified customer. It produces exactly three artifacts: **a pitch, a meeting
547plan, and the PDFs to deliver.**
548
549Full workflow in `references/meeting-prep.md` — mining what the customer already
550told us (read chat message **bodies**, never the auto-generated titles), finding
551the gap between what they promise publicly and what their published process
552delivers, the four document shapes, and the house format.
553
554Two things that are easy to get wrong and expensive to undo:
555
556- **Never bring an account-health / account-status PDF to a prospect.** That
557 format is internal tooling; run it on a quiet account and you hand someone an
558 audit of their colleague's inactivity.
559- **Data documents survive the call; argued documents often need re-aiming.**
560 Invest in the measured ones, and argue only from what the customer already did,
561 never from an assumed goal.
562
563## Guardrails
564
565- **Keep preparation separate from delivery.** Enriching/advancing Attio and
566 disabling a **confirmed-dead** task follow the existing limits; still **confirm
567 before moving a lead to Lost or disabling a task**. Sending an email additionally
568 requires explicit approval of that exact draft, through any tool or sender.
569- **Never mutate customer data with raw SQL — go through the API.** An `UPDATE` on
570 `scheduled_actions` writes the row but skips everything the endpoint does around
571 it: `updateTask` re-syncs the Trigger.dev schedule, and `deleteArtist` runs a
572 last-owner check plus a fail-closed song-dependency guard before hard-deleting.
573 A direct write leaves the row and the scheduler disagreeing, and the drift is
574 invisible in the table you just edited. Use `PATCH`/`DELETE` with an `account_id`
575 override. This applies to *every* customer-facing table, not just tasks.
576- **Draft first; no unapproved sends.** Follow `references/outreach-approval.md`
577 for every email. No quota, deadline, prior send, CRM stage, subagent recommendation,
578 or automatic goal continuation substitutes for the user's approval. Do not send
579 an apology or correction for an accidental send without review either.
580- **PII stays in the CRM.** Names, emails, account IDs live in Attio/Supabase —
581 never paste them into shared docs or external services.
582- **Exclude test rows** everywhere (`sweetmantech*`, `sidney@`, `@example.com`,
583 `[TEST]`, `preview-auth-probe`).
584- **Trust Stripe over the DB for paying status** — the Supabase `subscriptions`
585 mirror can be empty/stale.
586
587## Lessons from the first live run (2026-07-23)
588
589Four leads worked end to end: an inbound valuation lead, a distributor/broker, a
590major-label exec, and a dormant power user. What actually mattered.
591
592### Qualification
593
594- **Credit burn is not engagement.** Two accounts looked "active today" and were
595 pure autopilot — the usage was a scheduled task firing. One had been dark **4 months**
596 while its daily task quietly burned the balance negative. Check the last *interactive*
597 chat date, always (step 7 has the two tests that survive a NULL topic).
598- **And a quiet account is not a disengaged one.** A subscriber of **19 months** read as
599 dormant — 1 active day, 0 chats — because he had **456 credits against a 9,999
600 entitlement** the whole time and nobody had ever read his balance to trigger the refill.
601 He had paid $405.46 for roughly 2% of one month's allowance. Fleet-wide, **1,499 of
602 1,646** accounts were carrying an unapplied refill and **133 were blocked at or below
603 zero**. Before writing anyone off, check whether we ever funded them.
604- **Qualify the catalog before spending human time.** A lead who ran a valuation
605 and asked "what can I sell for" held a 10-track **public-domain classical**
606 catalog worth ~$700 — no publishing to sell, since Bach and Chopin are public
607 domain. Capture works fine; qualification is the gap. Check repertoire type and
608 streams before drafting anything.
609
610### Valuation you can put in front of a buyer
611
612- **Say "asset value, not a bid."** A catalog buyer asked "is this for the buy
613
614…(truncated)