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/leadsaccepts HMAC-signed seller leads, validates with Zod, requires consent/source/landing-page evidence, scores quality, and writesleads,lead_consent, andaudit_log. Signed idempotency events move fromprocessingtocompletedthrough 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 retryable409, 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 stayspending, 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, andPOST /admin/leads/:id/reviewlet 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.sourceretains page/campaign/manual input, whileleads.acquisition_sourceis exactcold | 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;
organicandppctherefore remaininboundduringprocessingandfinalizingrecovery 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-signatureHMAC-SHA256 over the raw JSON body withFHC_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
externalSourceIdor 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 rawsourcecontains an apparently paid or organic campaign name. - Authenticated Admin manual intake and review may assign only exact
cold,organic,ppc, orpendingvalues. Omission remains pending; near-miss and campaign-like values are rejected. organicmaps to Warm,ppcmaps to Hot, andcoldmaps 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, orppcclassification in its strict 24-column contract; absent, unknown, malformed, or extra columns fail the entire batch before writes. Imported rows remainpending_reviewuntil 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_keyis the database-wide concurrency fence for FHC, manual, and CSV writers. Migration0031deliberately 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, apply0031, 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 through0038remains immutable and0039_package_readiness_v4.sqlinstalls the current authority. The staging Worker records only literalpackage-reserve-v4decisions over the prior 30 complete UTC dates; omitted dates contribute zero. Its denominator selects qualifying PPC$250investor 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 eachactiveorcancel_at_period_endPackage plus6 * weekly_capfor each activemax_bid_cents >= 25000Territory, regardless of filters;weekly_cap = 0fails 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 mainfe6ff71…is staged only through0039; its first real v4 scheduler decision isready=1/reserve_satisfiedwith demand/committed0and supply5. 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:
- State + county + distress type + quality band.
- State + metro + distress type + quality band.
- State + distress type + quality band.
- 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_centsand optionallyleads.asking_price_cents.lead_price_historywith old/new values, actor, reason, and source.audit_logactionlead.price_set,lead.price_override, orlead.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_logrow for the batch and onelead_price_historyrow per affected lead.
6. Review, Listing, and Visibility
Recommended lifecycle:
captured: FHC KV/admin form received the lead.ingested: Soldi stored lead, consent evidence, audit row, and quality score.priced: price band or admin override assignedmarket_price_cents.reviewed: admin approved/rejected/clawed back.listed: available in Market or auto-allocated by Standing Order.sold: portfolio/delivery/charge ledger created.expiredorwithdrawn: 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_SECRETto 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_hashin 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
IngestLeadSchemashape 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 testfocused 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, andaudit_logatomically. - 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.jsonchas 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.ccafter 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:fhcandcd sites/fhc-pages && bun run auditpass with zero blockers.- Rendered output contains no
soldistring.
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-adminis credited and superseded where main already moved on.- The completion tracker points A1/A2/A3/A4 at this PRD.
12. Recommendation
build_next