# Recoup Internal Sales

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

- Skill: `recoupable-skills/recoup-internal-sales` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds add recoupable-skills/recoup-internal-sales`
- Raw SKILL.md: https://api.skillmd.com/api/skills/recoupable-skills/recoup-internal-sales/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: recoupable (https://skillmd.com/u/recoupable-skills)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/recoupable-skills/recoup-internal-sales

---


# 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:**

1. **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.
2. **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.
3. **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.
4. **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.

```sql
-- 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

1. **Collapse to people.** Key every signal by account/email and merge across the
   eight pulls — one person, all their signals, deduped.
2. **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.
3. **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.
4. **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.

1. **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.
2. **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](https://www.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.
3. **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.
4. **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.
5. **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).
6. **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)
