PRD - Supply Side, Admin Intake, and Pricing Controls

Status: LIVE-IN-PROGRESS - A0/A1/A2/A3 proven; A4 spec-only GREEN; payouts PENDING_CAM Owner: supply/admin Last updated: 2026-07-16

Source refs: docs/content/LEAD_FUNNEL.md, app/worker/routes/ingest.ts, app/worker/routes/admin-leads.ts, app/src/pages/Admin.tsx, sites/fhc-pages/src/index.ts, .wrkts/supplier-admin/docs/content/prd/ADMIN_CONTROLS.md, artifacts/soldi-completion-2026-07/GOAL_LOOP_PROMPT.md.

1. Purpose

Soldi has a buyer marketplace and an admin review console, but the supply path is not complete. Fair Home Cash captures seller leads into KV and emails the team, then stops. Soldi has a tested HMAC ingest endpoint that can score, store, review, list, and auto-allocate leads, but no production caller forwards leads into it.

This PRD defines the smallest honest supply-side plan:

  • Capture from Fair Home Cash into Soldi without losing the existing KV fallback.
  • Let admins add leads manually when a seller arrives by phone, referral, or offline ops.
  • Price leads before they list, using auditable defaults and explicit overrides.
  • Show admins where every lead sits from capture through sold/expired.
  • Keep seller payouts spec-only until the business model is decided.

2. Current State

Built today:

  • POST /api/v1/ingest/leads accepts HMAC-signed seller leads, validates with Zod, requires consent/source/landing-page evidence, scores quality, and writes leads, lead_consent, and audit_log. Signed idempotency events move from processing to completed through an immutable completion generation in the same lead batch; its trigger aborts the batch unless the exact event still belongs to that lead, so a worker superseded after the 10-minute lease cannot commit success. Only a completed event with its durable lead can replay successfully. Fresh processing returns retryable 409, while a leadless processing claim older than 10 minutes can be exact-lease reclaimed; completed-orphan reclaim preserves prior immutable completion history while permitting a new generation. FHC attribution is untrusted and stays pending, so this ingress cannot list or auto-allocate; Admin classification can make a priced approval listable.
  • GET /api/v1/admin/leads/pending, /admin/audit-log, /admin/compliance-summary, and POST /admin/leads/:id/review let admins review pending leads and record audit rows.
  • The Admin UI shows pending lead review, refund queue, compliance summary, and audit log.
  • The Market buy path and Standing Order allocation use integer cents and create wallet ledger, portfolio, delivery, and stage records.
  • A1/A2 proof now exists for FHC bridge forwarding, backlog drain, Admin manual intake, table-backed price bands, price override, approve-to-Market, batch discount, audit rows, and cleanup.
  • A3 proof now exists for Admin supply-funnel counts plus per-lead source, consent evidence, quality, review status, current price, and price history.
  • Canonical acquisition is independent from raw provenance: leads.source retains page/campaign/manual input, while leads.acquisition_source is exact cold | organic | ppc | pending. Unknown values fail closed; no historical campaign slug is classification evidence.
  • Expired Territory allocation recovery passes the persisted classified acquisition source through both tier economics and Standing Order source filters; organic and ppc therefore remain inbound during processing and finalizing recovery instead of leaking toward Package/Market fallback.

Missing today:

  • There is no seller-facing status page; seller visibility stays spec-only until Decision 0 is resolved.
  • There are no seller payout rails, and no Stripe Connect posture has been chosen.

3. Decision 0: Seller Business Model

docs/content/LEAD_FUNNEL.md leaves the central seller promise unresolved:

  • Option A: pure lead marketplace. Seller submits, Soldi/FHC matches the lead to buyer partners, and the buyer transacts.
  • Option B: principal or committed-offer posture. Soldi/FHC or committed partners make a cash-offer promise before resale/assignment.

Recommended default for all build work until Cam decides: pure lead marketplace semantics. Build intake, review, pricing, and buyer delivery in a way that is also compatible with a later principal posture, but do not ship seller-facing payout promises, guaranteed-offer wording, or money-out rails.

Tracker state for this decision: PENDING_CAM.

4. Intake Sources

A. Fair Home Cash bridge

Fair Home Cash remains the consumer capture surface. After KV write succeeds, it should call Soldi ingest in ctx.waitUntil:

  • Endpoint: https://app.soldi.cc/api/v1/ingest/leads.
  • Auth: x-fhc-signature HMAC-SHA256 over the raw JSON body with FHC_INGEST_SECRET.
  • Durability: KV write stays first and remains the fallback. A failed forward must not break seller form submission.
  • Evidence: forward result status and the generated Soldi lead id should be logged server-side without printing secrets.
  • Deduplication: include a stable externalSourceId or deterministic source key in a future migration before replaying large backlogs. Until that column exists, the drain script must be idempotent by reading/writing a replay ledger.

B. Admin manual entry

The Admin console needs an "Add lead" action for phone/referral/offline leads. It should write through the same validation, scoring, pricing, consent, review, and audit path as FHC ingest. Admin-entered leads require consent/source evidence, even if the source is admin_manual.

Minimum fields:

  • Property: address, city, state, zip, county.
  • Seller/contact: owner name, phone, email.
  • Situation: distress type, timeline/detail, property type, occupancy if known.
  • Valuation: estimated value, mortgage balance, optional asking price.
  • Consent/source: consent method, consent timestamp, source, landing page or intake channel note.
  • Pricing: explicit price or price-band default.

Non-goal: a public seller self-serve Soldi app in this phase.

C. Acquisition classification boundary

  • FHC/HMAC ingress always lands as pending, even when raw source contains an apparently paid or organic campaign name.
  • Authenticated Admin manual intake and review may assign only exact cold, organic, ppc, or pending values. Omission remains pending; near-miss and campaign-like values are rejected.
  • organic maps to Warm, ppc maps to Hot, and cold maps to Cold only after canonical classification. Pending has no buyer tier.
  • Raw source and landing-page evidence remain unchanged for audit and attribution analysis.
  • Admin CSV import is implemented and locally proven on the v60 launch train. It accepts only the exact cold, organic, or ppc classification in its strict 24-column contract; absent, unknown, malformed, or extra columns fail the entire batch before writes. Imported rows remain pending_review until an Admin explicitly approves them.
  • Every current FHC, Admin-manual, and Admin-CSV ingress persists the same application-owned NFKC/case property identity as a compact canonical JSON array. The encoding is injective even when a field contains a delimiter. Admin CSV performs an indexed historical lookup on that complete key and fails closed before writes if any legacy lead remains uncanonicalized; finite raw/full-width compatibility enumeration is not used.
  • A partial unique index on every non-null leads.property_identity_key is the database-wide concurrency fence for FHC, manual, and CSV writers. Migration 0031 deliberately aborts before creating it if the provably safe historical backfill produces a duplicate. Before staging or production import enablement, run the read-only safe-subset duplicate preflight, apply 0031, backfill remaining null keys through application-owned NFKC code, resolve any unique conflicts, and prove the null count is zero. Admin import stays fail closed until that gate passes.

5. Pricing Model

Recommended default: price bands plus admin override.

Package reserve and Market batch controls (staging-backed v4)

  • Merged backend integrity owns 0032; forward-only history through 0038 remains immutable and 0039_package_readiness_v4.sql installs the current authority. The staging Worker records only literal package-reserve-v4 decisions over the prior 30 complete UTC dates; omitted dates contribute zero. Its denominator selects qualifying PPC $250 investor leads in-window (Territory-claimed supply is not subtracted); malformed timestamps inside that set fail closed, while noncandidate PPC rows do not poison supply. Its numerator is 25 for each active or cancel_at_period_end Package plus 6 * weekly_cap for each active max_bid_cents >= 25000 Territory, regardless of filters; weekly_cap = 0 fails closed. Scheduler and paid activation/renewal are the two enforcement gates; create/raise remains one-sided and can make Package readiness not-ready without blocking the Territory. The verbatim owner authority and accepted caveats are in Package reserve handoff.
  • Package fulfillment accepts only literal APP_ENVIRONMENT=staging, literal scheduler enablement, and the attested v4 ready row; undefined, unknown, test, development, and production configurations all fail closed even when a row is copied. The paid activation/renewal transaction then unconditionally rechecks live current denominator, commitments, and prospective +25 before it can debit the wallet; renewal excludes its exact predecessor so replacing one 25-lead commitment records 25 rather than 50. A D1 trigger rejects a paid cycle with no v4 reservation. Exact main fe6ff71… is staged only through 0039; its first real v4 scheduler decision is ready=1 / reserve_satisfied with demand/committed 0 and supply 5. That is not funded-activation, Stripe/webhook, wallet, or delivery proof; no manual readiness row or wallet, Package, or lead mutation occurred.
  • Direct Market batch purchase is a separate 2–25 lead server contract. The batch owns every lead and makes one wallet debit or commits nothing. Cold/Warm may use only the already documented aggregate 3+/5+/10+ ladder; Hot remains $250. Buyer UI and promotional copy remain disabled until reset-isolated hosted proof plus an explicit UI-lane activation decision.

Why: FHC form data is incomplete and market-specific, while IngestLeadSchema requires marketPriceCents. A deterministic default keeps the bridge moving without pretending the first price is final.

Price bands

Add a table in A2:

CREATE TABLE lead_price_bands (
  id TEXT PRIMARY KEY,
  state TEXT NOT NULL,
  metro TEXT,
  county TEXT,
  distress_type TEXT NOT NULL,
  min_quality_score INTEGER NOT NULL DEFAULT 0,
  max_quality_score INTEGER NOT NULL DEFAULT 100,
  market_price_cents INTEGER NOT NULL,
  asking_price_cents INTEGER,
  status TEXT NOT NULL DEFAULT 'active',
  created_by_user_id TEXT REFERENCES users(id),
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

Matching precedence:

  1. State + county + distress type + quality band.
  2. State + metro + distress type + quality band.
  3. State + distress type + quality band.
  4. Global distress type fallback.

If no band matches, the lead lands as pending review with market_status='pending_review' or equivalent and does not list until an admin sets a price. If the current schema cannot represent unpriced pending leads without violating IngestLeadSchema, A1 should compute a conservative provisional price from a global fallback and mark the audit metadata as pricingSource: "fallback_band".

Admin override and price history

Every price mutation writes:

  • leads.market_price_cents and optionally leads.asking_price_cents.
  • lead_price_history with old/new values, actor, reason, and source.
  • audit_log action lead.price_set, lead.price_override, or lead.discount_batch.

Money stays integer cents end to end.

Discount batch

Admin batch action for stale unsold leads:

  • Filter by state/county/distress type/source/age/quality.
  • Show count, current average price, proposed new average price, and total potential discount.
  • Dry run first. Apply only after admin confirmation.
  • Write one audit_log row for the batch and one lead_price_history row per affected lead.

6. Review, Listing, and Visibility

Recommended lifecycle:

  1. captured: FHC KV/admin form received the lead.
  2. ingested: Soldi stored lead, consent evidence, audit row, and quality score.
  3. priced: price band or admin override assigned market_price_cents.
  4. reviewed: admin approved/rejected/clawed back.
  5. listed: available in Market or auto-allocated by Standing Order.
  6. sold: portfolio/delivery/charge ledger created.
  7. expired or withdrawn: not currently built, but needed for stale supply management.

Admin supply-funnel view:

  • Counts by lifecycle state.
  • Drill-in per lead: source, consent, quality score, current price, price history, review history, Market status, buyer/delivery if sold.
  • Clear "needs price", "needs review", and "ready to list" queues.

Buyer visibility:

  • Before purchase: masked address, city/state/zip, distress type, property facts, quality score, price, and consent summary.
  • After purchase: seller contact data and consent proof needed for outreach.

Seller visibility:

  • Spec-only for now. The safest minimal future version is a tokenized status page linked from the FHC auto-reply email with neutral states such as "received", "reviewing", "matched with buyer partner", and "closed/no match".
  • Do not promise a payout amount, guaranteed offer, or closing timeline until Decision 0 is resolved.

7. Payout Posture

Seller payouts are A4 spec-only. Do not build Stripe Connect, bank onboarding, identity, payouts, refund-to-card, or money-out tables in this program.

If Cam chooses pure lead marketplace:

  • Soldi/FHC sells buyer access to leads.
  • Seller payment happens outside Soldi between seller and buyer or through a later documented transaction model.
  • Soldi may charge a flat marketing/access fee to buyers.

If Cam chooses principal/committed-offer:

  • Requires counsel review, operating entity posture, funding source, buyer partner contract terms, assignment/wholesaling compliance, and Stripe Connect or a non-Stripe payout path.
  • Requires a separate PRD before implementation.

8. API and Schema Plan

A1 bridge

  • Add FHC_INGEST_SECRET to both workers.
  • Add server-side FHC forward helper and HMAC signing.
  • Add a backlog drain script or admin action.
  • Prefer adding external_source_id / source_payload_hash in Soldi if idempotent replay cannot be proven with existing fields.

A2 admin intake/pricing

New or extended routes under /api/v1:

  • POST /admin/leads - manual lead create through the same ingest service.
  • PATCH /admin/leads/:id/price - set/override market and asking price.
  • GET /admin/price-bands - list bands.
  • POST /admin/price-bands - create/update bands.
  • POST /admin/leads/discount-batch/dry-run - preview stale-lead repricing.
  • POST /admin/leads/discount-batch/apply - apply repricing with audit rows.

All request bodies use Zod. Every price value is integer cents.

A3 supply visibility

New or extended route:

  • GET /admin/supply-funnel - counts and queues for captured/ingested/priced/reviewed/listed/sold/expired plus recent exceptions.

9. Prior Art Reconciliation

.wrkts/supplier-admin/docs/content/prd/ADMIN_CONTROLS.md is valuable for:

  • Admin auth using users.admin.
  • Consent-first ingest.
  • Atomic audit rows.
  • Buyer exposure rules.
  • CLI/manual admin posture.

Superseded pieces:

  • CLI-first only is no longer sufficient because the live Admin React console exists.
  • Its "no admin React UI" non-goal is superseded by current main.
  • Seller suspension remains blocked on a supplier identity model and should stay out of A1/A2.

.wrkts/checkout-stripe/docs/content/prd/PAYMENTS.md is adjacent only. It informs Stripe customer/receipt posture but does not unblock supply-side intake or pricing.

10. Testing and Validation Plan

Unit tests

Success criteria:

  • FHC payload mapper converts form/KV fields into IngestLeadSchema shape without raw string money.
  • HMAC signer uses the exact raw JSON body.
  • Price-band matcher follows precedence and returns integer cents.
  • Discount batch calculator returns deterministic before/after rows.

Validation:

  • bun test focused worker/helper tests.
  • Negative tests for missing consent evidence, missing price band, invalid cents, and duplicate replay key.

Integration tests

Success criteria:

  • FHC bridge call persists a lead, consent row, review/audit row, and either a listed price or pending-price state.
  • Admin manual add-lead writes the same evidence rows.
  • Admin price update writes leads, lead_price_history, and audit_log atomically.
  • Discount batch dry-run does not mutate; apply mutates exactly the previewed rows.
  • Non-admin users receive 403 for every admin route.

Validation:

  • Worker route tests with fake D1 metadata where current harness requires it.
  • Local D1 migration apply for any new tables.
  • bun run verify.

Programmatic production checks

Success criteria:

  • wrangler d1 migrations list soldi --remote --config app/wrangler.jsonc has no unapplied migrations after merge/deploy.
  • One FHC test submission produces a remote Soldi lead row with consent evidence and source metadata.
  • Backlog drain reports submitted, skipped duplicate, failed, and retried counts.
  • Audit rows can be queried for each mutation.

Validation:

  • Save D1 query output to artifacts/soldi-completion-2026-07/shots/.
  • Use only names-only secret proofs for FHC_INGEST_SECRET.

Browser E2E

Success criteria:

  • Admin can add a lead, set/override its price, approve it, and see it move through the supply funnel.
  • Market shows approved/priced leads only.
  • A buyer purchase or Standing Order allocation creates wallet charge, portfolio, delivery, and stage rows.
  • Admin funnel and lead drill-in render with zero console/page errors at desktop and 375px.

Validation:

  • Chrome/Playwright proof on app.soldi.cc after merged PRs and authorized deploys.
  • Screenshots and network receipts saved under artifacts/soldi-completion-2026-07/shots/.

FHC audit

Success criteria:

  • Bridge-only FHC diff changes no rendered page copy/disclosure.
  • bun run build:fhc and cd sites/fhc-pages && bun run audit pass with zero blockers.
  • Rendered output contains no soldi string.

Validation:

  • Hash or diff generated HTML if needed to prove no copy movement.
  • Deploy FHC only for the authorized A1 bridge slice.

11. Acceptance Criteria

A0 is complete when:

  • This PRD is merged.
  • Decision 0 is explicitly logged as PENDING_CAM.
  • The recommended defaults are documented: FHC bridge plus admin manual intake, price bands plus admin override, admin-only supply visibility first, payouts spec-only.
  • .wrkts/supplier-admin is credited and superseded where main already moved on.
  • The completion tracker points A1/A2/A3/A4 at this PRD.

12. Recommendation

build_next