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() from workspace/scripts/hubspot-deal-creator.js. Parallel implementations have caused data drift and duplicate records in the past.

Quick reference

FieldValue
StagesSource 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 chainSource adapter → hubspot-deal-creator.js → HubSpot CRM → outreach-stage1
Compliance gates listn/a (pre-send; compliance gates fire at outreach stage)
Skills invokedhubspot-deal-ingest, il-marketplace-pull
Success metricscreateDealFromParsed() 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 resultSee acquisition_deals Supabase table, most recent created_at
Failure modesHUBSPOT_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).

PipelineIDOwnerFirst stagePurpose
Acquisition877963314aurora@reri.coSource-specific (see table below)CH-side: where deal originates, outreach tracked here
Wholesale/Dispo816046aurora@reri.co1006885379 (New Property)Buyer-side: blast to buyers when ready to dispo

Acquisition pipeline first-stage mapping

SourceFirst stage
investorliftnew_deal_investorlift
facebooknew_deal_facebook
connected_investorsnew_deal_connected_investors
salesmsgnew_deal_salesmsg
openphone_inbound / openphonenew_deal_openphone
referral / cold_outreach / emailnew_deal_email
crexinew_deal_email (fallback)
propstreamnew_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

SourceAdapterNotes
InvestorLiftworkspace/scripts/workers/acq-deals-hs-syncer.jsReads 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.
Facebookworkspace/webhooks/hubspot-handler.js (inbound)Facebook lead ads webhook → parsedDeal
OpenPhone inboundworkspace/webhooks/quo-handler-enhanced.jsCH calls/texts in → deal creation
SalesMsg inboundworkspace/webhooks/salesmessage-handler-v4-complete.jsSalesMsg inbound → deal creation
Manual / Crexi / PropStreamhubspot-deal-ingest — direct createDealFromParsed() callHenry pastes deal data → Claude constructs parsedDeal

Components

  • workspace/scripts/hubspot-deal-creator.js — canonical creator (1006 lines, 11 exports); NEVER reimplement
  • workspace/scripts/workers/acq-deals-hs-syncer.js — IL adapter (334 lines); reads Supabase, calls creator
  • workspace/scripts/lib/stage-sync.js — acq ↔ dispo HS stage sync on conversation events
  • workspace-acquisitions/FIELDS.md — RERI + IL custom property catalog
  • agents/acquisitions/agent/FIELDS.md — agent-side field reference
  • docs/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 → parsedDeal object produced → createDealFromParsed() called → dual-pipeline write → contact associated → acquisition_deals Supabase row updated → approval gate evaluated for outreach
  • Agents involved: _summary (primary orchestrator), _summary (field audit), _summary (inbound deal creation via webhooks)
  • Failure mode: isDuplicate: true returned → update existing deal, do not create; 429 from 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_deals Supabase row has hubspot_deal_id populated

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

Plans that govern this

Feedback rules

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

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.

HubRelationship
compliance-gatesDownstream: gates fire after deal is created
outreach-stage1Downstream: Stage 1 outreach triggered post-ingest
hubspotDual-pipeline destination for every deal
twilioCarrier layer for SMS post-ingest (10DLC B14 blocks)
salesmsgSalesMsg inbound creates deals here
openphone-quoOpenPhone 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.js must SSH to ec2-user@100.123.248.46. See CLAUDE.md InvestorLift section.
  • AWS Mac Ultra impaired since 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)