Deal Ingestion
Deal ingestion is the first step in every acquisitions workflow. Any lead source — InvestorLift, Crexi, PropStream, Facebook, manual paste, or future scrapes — produces a standardized parsedDeal object that flows through createDealFromParsed(). The function handles dedup, contact find-or-create, dual-pipeline HubSpot writes (acquisition + dispo), and deal-to-deal association atomically. Read this hub before working on outreach-stage1 or any code that creates HubSpot deals. Owner: acquisitions agent.
Rule: NEVER reimplement deal creation. Always call
createDealFromParsed()fromworkspace/scripts/hubspot-deal-creator.js. Parallel implementations have caused data drift and duplicate records in the past.
Quick reference
| Field | Value |
|---|---|
| Stages | Source ingest → parsedDeal adapter → dedup check → contact find/create → acq pipeline create → dispo pipeline create → deal-to-deal associate |
| Primary agent | _summary |
| Supporting agents | _summary, _summary |
| Agent handoff chain | Source adapter → hubspot-deal-creator.js → HubSpot CRM → outreach-stage1 |
| Compliance gates list | n/a (pre-send; compliance gates fire at outreach stage) |
| Skills invoked | hubspot-deal-ingest, il-marketplace-pull |
| Success metrics | createDealFromParsed() returns { deal, contact, isNewContact, isDuplicate, dispoDeal }; both pipelines created; 0 duplicate deals |
| Cost per stage | ~6 HubSpot API requests per deal (throttled ≥500ms/deal); ~$0.001 Supabase write |
| Throughput | ~50-200 deals/day from IL daily sync; rate capped by HubSpot 100 req/10s limit |
| Last run result | See acquisition_deals Supabase table, most recent created_at |
| Failure modes | HUBSPOT_PAT missing; 429 from HubSpot; parsedDeal missing sourcePhone; dispo deal fails after acq created (non-fatal, recoverable) |
Architecture
[any source: IL, Crexi, PropStream, FB, manual]
│
▼
adapter (per-source) → parsedDeal object (PARSED-DEAL-CONTRACT.md)
│
▼
hubspot-deal-creator.js → createDealFromParsed(parsedDeal)
│
├─→ dedup search (by source+source_id, then address)
├─→ contact find-or-create (by phone, last 10 digits)
├─→ create acquisition deal (pipeline 877963314)
├─→ create dispo deal (pipeline 816046)
├─→ associate contact ↔ both deals
└─→ associate acq ↔ dispo (association type 36)
│
▼
HubSpot CRM (portal 6193101)
│
▼
acquisition_deals (Supabase CCP mirror)
│
▼
→ outreach-stage1 pipeline (approval gate evaluation)
Dual-pipeline writes
This is atomic by design: both pipelines are always created together. Creating an acq deal without a dispo deal leaves the system in a broken state (no buyer-side record to blast).
| Pipeline | ID | Owner | First stage | Purpose |
|---|---|---|---|---|
| Acquisition | 877963314 | aurora@reri.co | Source-specific (see table below) | CH-side: where deal originates, outreach tracked here |
| Wholesale/Dispo | 816046 | aurora@reri.co | 1006885379 (New Property) | Buyer-side: blast to buyers when ready to dispo |
Acquisition pipeline first-stage mapping
| Source | First stage |
|---|---|
investorlift | new_deal_investorlift |
facebook | new_deal_facebook |
connected_investors | new_deal_connected_investors |
salesmsg | new_deal_salesmsg |
openphone_inbound / openphone | new_deal_openphone |
referral / cold_outreach / email | new_deal_email |
crexi | new_deal_email (fallback) |
propstream | new_deal_email (fallback) |
parsedDeal contract
Every source adapter must produce a parsedDeal object conforming to docs/PARSED-DEAL-CONTRACT.md. Minimum required fields:
{
address: '123 Main St, Phoenix, AZ 85001', // full street address
askingPrice: '250000', // CH asking price (string or number)
arv: '380000', // after-repair value
beds: 3, baths: 2, sqft: 1450,
wholesalerName: 'ACME Wholesalers LLC',
sourcePhone: '+15551234567', // CH phone — REQUIRED for contact create
dealSource: 'investorlift', // maps to first-stage
// ... 60+ additional optional fields (see PARSED-DEAL-CONTRACT.md)
}64+ fields total across 5 property families: Standard HS (8), RERI custom (14), InvestorLift (38), Property (~10), Inbound source tracking (5).
Source adapters
| Source | Adapter | Notes |
|---|---|---|
| InvestorLift | workspace/scripts/workers/acq-deals-hs-syncer.js | Reads acquisition_deals Supabase → supabaseDealToParsed() → createDealFromParsed(). Mandatory: runs via SSH to AWS Mac Ultra (ec2-user@100.123.248.46) for scraping; VPS IP is CloudFront-blocked. |
workspace/webhooks/hubspot-handler.js (inbound) | Facebook lead ads webhook → parsedDeal | |
| OpenPhone inbound | workspace/webhooks/quo-handler-enhanced.js | CH calls/texts in → deal creation |
| SalesMsg inbound | workspace/webhooks/salesmessage-handler-v4-complete.js | SalesMsg inbound → deal creation |
| Manual / Crexi / PropStream | hubspot-deal-ingest — direct createDealFromParsed() call | Henry pastes deal data → Claude constructs parsedDeal |
Components
workspace/scripts/hubspot-deal-creator.js— canonical creator (1006 lines, 11 exports); NEVER reimplementworkspace/scripts/workers/acq-deals-hs-syncer.js— IL adapter (334 lines); reads Supabase, calls creatorworkspace/scripts/lib/stage-sync.js— acq ↔ dispo HS stage sync on conversation eventsworkspace-acquisitions/FIELDS.md— RERI + IL custom property catalogagents/acquisitions/agent/FIELDS.md— agent-side field referencedocs/PARSED-DEAL-CONTRACT.md— authoritative input contract (every adapter must conform)docs/HANDOFF-FOR-KIMI.md— audit + dry-run task for Kimi
How it’s used
- Trigger: new lead arrives from any source OR Henry instructs “push this deal to HubSpot”
- Workflow: per-source adapter runs →
parsedDealobject produced →createDealFromParsed()called → dual-pipeline write → contact associated →acquisition_dealsSupabase row updated → approval gate evaluated for outreach - Agents involved: _summary (primary orchestrator), _summary (field audit), _summary (inbound deal creation via webhooks)
- Failure mode:
isDuplicate: truereturned → update existing deal, do not create;429from HubSpot → adapter throttles ≥500ms/deal;parsedDeal.sourcePhone=null→ contact creation skipped, deal still created - Success criteria: both acq + dispo deals exist in HubSpot; deal-to-deal association type 36 verified;
acquisition_dealsSupabase row hashubspot_deal_idpopulated
Cross-links
Agents that touch this
- _summary — primary orchestrator for deal creation and qualification
- _summary — property field audit, data enrichment oversight
- _summary — creates deals from inbound webhook events (openphone, salesmsg)
- _summary — reads dispo pipeline; triggers blast when deal ready
Skills that invoke this
- hubspot-deal-ingest — canonical entry point for all manual/semi-manual ingest
- il-marketplace-pull — IL data layer; produces deals that feed acq-deals-hs-syncer
- acquisitions-outreach — consumes deals created here (reads
acquisition_deals) - dispo-blast — consumes dispo deal records created in pipeline 816046
Plans that govern this
- openclaw-fragmentation-fix-2026-05-01 — CHOKEPOINT-1/2/3 governance (all state writes through Supabase)
- vendor-deep-audit-comprehensive-2026-05-02 — HubSpot field mapping audit pending
Feedback rules
- feedback_no_assumptions — verify HubSpot field names against live API before writing
- feedback_dual_write_required — both acq + dispo pipelines must be created together
- feedback_chokepoint_principle — state mutation through Supabase single surface
- feedback_verify_schema_before_designing — check PARSED-DEAL-CONTRACT.md before adapter work
- feedback_audit_before_architect — search for existing adapter before creating new one
KB / source docs
- README — HubSpot API KB (13 files including ASSOCIATIONS-API.md, CALLING-SDK.md, COMMUNICATIONS-API.md)
- README — IL marketplace data layer
- README — Supabase CCP project schema
System maps
- vm-data-flow — data ingestion and pipeline overview
- vm-acq-agent-flow — acquisitions agent ingest flow
Related: SMS/Carrier compliance cluster
Deal ingestion is upstream of outbound SMS. After a deal is created here, the compliance gates in compliance-gates evaluate before any SMS is sent.
| Hub | Relationship |
|---|---|
| compliance-gates | Downstream: gates fire after deal is created |
| outreach-stage1 | Downstream: Stage 1 outreach triggered post-ingest |
| hubspot | Dual-pipeline destination for every deal |
| twilio | Carrier layer for SMS post-ingest (10DLC B14 blocks) |
| salesmsg | SalesMsg inbound creates deals here |
| openphone-quo | OpenPhone inbound creates deals here |
Open issues / TODOs
- 8 unmapped IL detail fields pending HS admin creation:
matterport_url,zestimate,roof_age,foundation_condition,heating_system_age,hunter_email_score,hunter_linkedin_url,apollo_employee_count - Owner assignment policy: currently aurora@reri.co for all deals; future: route by state or per-source inbox owner
- Re-ingest cadence: upsert currently overwrites all fields — need to preserve human-edited fields (open decision)
- InvestorLift scraping ALWAYS via AWS Mac Ultra — VPS IP is CloudFront-blocked (403).
acq-deals-hs-syncer.jsmust SSH toec2-user@100.123.248.46. See CLAUDE.md InvestorLift section. - AWS Mac Ultra
impairedsince 2026-05-02 22:15 UTC — IL daily sync paused until instance rebooted via AWS Console.
Recent activity
- 2026-05-03: hub created (W1-S8)