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-08-22
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:
FHC capture remains fail-closed at
pending_review; a first-party source or provider certificate does not itself authorize listing or allocation. Admin review owns acquisition classification and price confirmation before existing Territory/Market transitions may run.FHC omits
marketPriceCents, so Soldi price bands plus audited Admin override remain the sole buyer-economic authority.quality_scoreis independent from acquisition tier: only canonicalcold | organic | ppcclassification maps to Cold/Warm/Hot.Canonical property identity prevents a second sellable row for the same normalized address/city/state/ZIP. Seller-facing repeat-submission merge/supplement behavior remains an explicit product decision; phone-only deduplication is not safe.
The 2026-08-22 FHC source candidate makes TrustedForm bootstrap coverage and connector disclosure mandatory on every transactional generated page. Provider account, hosted certificate, downstream storage, and Retain proof remain release gates; see
docs/content/owners/zak/FHC_CAPTURE_ROUTING_HANDOFF_2026-08-22.md.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.Durable ad-attribution evidence (2026-08-20, source only): FHC forms capture
utm_source/medium/campaign/content/term,gclid,fbclid,referrer, and the browserevent_id; the signed forward carries them as a sanitized optionalattributionobject; ingest persists an append-onlylead_attributionrow (migration0048, immutability triggers) in the same batch as the lead; and the Admin pending queue returns the evidence beside consent. The Admin review card now requires an explicitcold | organic | ppcselection before Approve, flowing through the existingacquisitionSourcereview parameter. Evidence never auto-classifies — FHC ingress still landspending.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, property facts, acquisition tier, price, and consent summary. Distress/situation taxonomy is absent from both the Market API and buyer UI.
- After purchase: seller contact data, situation context, 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