# Clay-Powered Enrichment Waterfall — Amazon Seller Distress Signal (Portfolio Build)

**Built:** 2026-07-07 · **Data:** 100% synthetic, generated for this project · **Status:** complete

## What this proves

**All data in this repo is synthetic, and the ~70% figure below is a modeled
reduction in rows eligible for paid execution, not measured production cost.**
The consumption section states the assumption and the missing billing evidence.

Built a qualification gate that reduces the rows eligible for downstream paid
enrichment from 20 to 6 in this synthetic fixture, or **70%**. That is a
row-volume result, not a measured credit or dollar saving. The gate's
false-negative rate is untested, so the build does not show that useful prospect
coverage was preserved.
The same `signal_score` field is the upstream input to
[revenue-engine-demo](https://github.com/Aditya-chouhan/revenue-engine-demo),
where it drives CRM lead-routing priority — turning a one-off enrichment
exercise into the front end of a full lead→revenue pipeline.

## Run it yourself

```
python3 signal_score.py
```

Standard library only — no API key, no network, no dependencies. Writes
`output/signal_priority.csv` and `output/signal_priority_beachhead.csv`, and
reproduces `baseline-output/console-output.txt` exactly.

## What this is

This project demonstrates the qualification logic, documented Clay formula
design and one live agentic-enrichment request. It does not demonstrate a
persistent Clay table, provider-positive enrichment or a verified cost ledger.

The scoring logic is a velocity-deceleration detector: it finds Amazon sellers
whose rating cadence just collapsed (the WHEN-is-this-seller-in-pain signal,
distinct from a plain WHO-fits-the-ICP score). That mechanism was designed for
seller signal detection in a prior GTM engineering role and is reimplemented
here from scratch in this repo's own `signal_score.py`. **No employer code,
data, or credentials are used anywhere in this repo.**

The build ports that scoring math into Clay formula columns as a pre-enrichment
qualification gate, then designs a cheapest-to-most-expensive contact-finding
waterfall that only makes rows eligible for enrichment after they qualify —
"score first, then enrich the rows that qualify."

## Tool-surface note (read this first — it shapes what "built in Clay" means here)

The Clay MCP connected to this account exposes Clay's **Audiences /
agentic-enrichment API** (`find-and-enrich-company`, `find-and-enrich-list-of-contacts`,
`add-contact-data-points`, `run_subroutine`, `ask-question-about-accounts`) —
this is a real, live connection and it was used for real (see Section B below).
It does **not** expose a tool for creating a persistent spreadsheet-style table,
defining formula columns, setting per-column conditional run rules, or
exporting a table to Google Sheets — the classic Clay grid-builder UI has no
API surface in this MCP.

So the "table" in this build exists in two forms, both real and both checked
against each other:

1. **`clay-formula-columns.md`** — the exact Clay formula syntax for every
   column in the waterfall, in order, paste-ready into a live Clay table.
   This is the actual formula-column competency artifact.
2. **`data/clay-table-full.csv`** and **`data/clay-waterfall-portfolio-demo-output.csv`**
   — the computed table *state* (every column's value, every row), generated
   by re-implementing the exact same formulas in Python so the numbers are
   provably correct before they're ever pasted into Clay's UI.
3. **The contact waterfall itself was run for real** through the live Clay
   MCP (`find-and-enrich-list-of-contacts`, `Email` data point) against the
   6 qualifying synthetic sellers — this is genuine hands-on tool use, not a
   simulation. Result: all 6 returned `notFoundContacts` (task id
   `mcp-task_0thsve7Bf6u4NU9vANz`), which is the correct, expected outcome —
   these are fictional people at `.example.com` domains that don't exist in
   any real provider's index. That result is exactly what triggers the
   step-4 AI-guess fallback documented below.

No fabricated screenshots, no invented "column comments" pasted into a UI that
was never touched. What's real is labeled real; what's a faithful Python
re-implementation of a documented Clay formula is labeled as that.

## A — Scoring / qualification columns (the pre-enrichment gate)

Full formula translation table: see `clay-formula-columns.md`. Summary of the
11 formula columns: `m90_monthly`, `m365_monthly`, `qualifies` (boolean gate),
`monthly_lost`, `recent_drop`, `sustained_drop`, `f_magnitude`, `f_reputation`,
`signal_score` (0–10), `decay_shape` (freefall / slow-bleed / recent-break),
`in_beachhead` (Beauty/Baby).

**Why this order matters:** every proposed waterfall enrichment step is
conditionally gated on `qualifies = TRUE`. The qualification formula itself
does not purchase a provider data point, while Clay may still account for
platform Actions according to workspace configuration. Only sellers already
showing the defined velocity collapse become eligible for contact finding.

## B — Waterfall contact-finding columns (conditional, cheapest → most expensive)

| Step | Method | Run condition |
|---|---|---|
| 1 | Clay native free/cheap company+person match | `qualifies = TRUE` |
| 2 | Hunter.io (mid-cost finder — substituted for the brief's generic example; this is what's available in the connected workspace) | step 1 blank |
| 3 | Apollo people-match (premium finder) | steps 1–2 blank |
| 4 | Claygent AI-guessed pattern (`first.last@domain`), flagged LOW confidence, never sent without human verification | steps 1–3 blank |

**Live result (2026-07-07, real MCP call):** all 6 qualifying synthetic
sellers fell through to step 4 — steps 1–3 legitimately found nothing because
the decision-makers and domains are fictional. Each `waterfall_email` in
`data/clay-table-full.csv` is a LOW-confidence guessed pattern, exactly as
specified — this correctly demonstrates the fallback firing, not a failure of
the build.

`contact_source` on every row records which step produced the result (or
`not_run` for the 14 non-qualifying rows) — the auditability trail.

## C — Consumption model (and what was not measured)

Clay's current documentation separates **Actions** (platform work) from
**Data Credits** (purchased data). An enrichment can consume both, and the
actual amount depends on the configured enrichment, reruns and provider. See
[Clay's Actions & Data Credits documentation](https://university.clay.com/docs/actions-data-credits).

**Measured in this build:**
- 20 synthetic rows were scored.
- 6 qualified (30%) and entered the live contact-enrichment request; 14 were
  gated out.
- No contacts were found for the six fictional people.

**Not measured:** the committed artifacts do not include a workspace billing
or usage export. They therefore do not establish the number of Actions, Data
Credits, provider charges, dollars spent or cost per qualified lead. A failed
lookup is not assumed to be free without an actual ledger entry.

**Hypothetical row-volume effect:** if the same paid operation would otherwise
run once on every row at a constant per-row consumption, gating 14 of 20 rows
reduces eligible paid executions from 20 to 6, or **70%**. The earlier
350-credit versus 105-credit example was this arithmetic with an assumed 17.5
credits per row; it was not a measured bill and is no longer presented as one.
Actual savings require the table's Actions, Data Credits, external-provider
charges, cache/rerun behavior and usable-contact yield.

## Before/after: Python baseline vs. Clay table — do they match?

`baseline-output/console-output.txt` (running `signal_score.py` directly
against `data/synthetic-sellers.csv`) and `data/clay-table-full.csv` (the
Clay-formula re-implementation) agree exactly:

- Both find **6 qualifying rows out of 20** (30%).
- Both rank them in the same order by `signal_score`:
  Hearthstone Home Co (7.43) → Voltix Gadgets (7.20) → Tiny Sprout Baby
  (7.18) → Glow Ritual Beauty (7.14) → Aurora Skin Co (7.09) → Bluebell Baby
  Goods (6.65).
- Both agree on `decay_shape` per row (2 freefall / 1 slow-bleed / 3
  recent-break) and on `in_beachhead` (4 of the 6 qualifiers: Aurora Skin Co,
  Bluebell Baby Goods, Glow Ritual Beauty, Tiny Sprout Baby).

No mismatch to debug — the formula translation is verified correct.

## Files in this project

| File | What it is |
|---|---|
| `data/synthetic-sellers.csv` | 20-row synthetic seed data, schema-matched to `signal_score.py` |
| `baseline-output/` | Local Python baseline run (console output + raw CSVs) |
| `data/clay-table-full.csv` | All 20 rows with formula outputs plus an explicitly assumed per-row credit field; not a billing export |
| `data/clay-waterfall-portfolio-demo-output.csv` | Final filtered/sorted export — the "Google Sheet" terminal output (CSV fallback; no Sheets MCP connected in this session) |
| `clay-formula-columns.md` | Paste-ready Clay formula syntax, in build order, for every column |

## What this doesn't prove — the honest gap

The 70% figure measures reduction in rows entering the modeled paid path under
the gate's own definition of "qualified." It says nothing about whether that definition is the *right*
one — whether the 14 rows the gate rejects would actually have converted at
a materially lower rate than the 6 it lets through. That's a different,
harder claim, and this build doesn't make it.

The rigorous check: enrich a random holdout sample of *rejected* rows anyway,
run real outreach against both groups, and compare downstream reply rate —
qualified vs. rejected. If rejected-row replies come in close to
qualified-row replies, the gate isn't discriminating value; it may be excluding
pipeline. If they come in well below, the gate is doing useful qualification.

This repo can't run that test — the data is synthetic, and the one real
Clay MCP call made against it (Section B) returned `notFoundContacts` on
all 6 qualifying rows, because the sellers are fictional `.example.com`
people with no real inbox to reply from. Zero real emails have been sent
in either direction, qualified or rejected, so there is no reply-rate data
to compare yet. **The gate's false-negative rate — whether it's silently
skipping good prospects, not just bad ones — is genuinely unmeasured, and
belongs next to the row-volume claim, not below it.** At 2,000+ real rows
(see below), this holdout is the first thing to run, before re-tuning
anything else.

## What would change at real 2,000+ row scale

At 20 rows the qualification gate is a convenience; at 2,000+ rows it's the
entire economic case for the build. Three things change: (1) the `qualifies`
formula and waterfall columns need to run as a scheduled Clay workflow against
a live HubSpot/Apollo-sourced import rather than a one-time CSV paste, so new
sellers get scored as they're discovered, not in a single batch; (2) the
consumption tracker needs to come from Clay's actual table/workspace usage and
external-provider bills, exported into a place where usable contacts per Action,
Data Credit and dollar can be tracked over time; and (3) step 1–3 provider order should be re-tuned using
real hit-rate data after the first few hundred rows — right now the order is
the brief's specified cheapest-first assumption, not something empirically
verified. A prospective comparison at real volume can test provider order and
usable-contact cost instead of relying on published averages.
