Purpose-Built ABM for Space Services
Unlike 6sense or Demandbase—which are built to be generic "intent + ads + orchestration" layers—SpaceABM is designed around the actual unit of work in space: a mission-driven procurement request. You can bolt custom fields and segments onto a horizontal ABM tool, but you still end up forcing space-specific reality (interfaces, orbit regimes, lead times, compliance gates, capacity windows, multi-leg service chains) through a marketing-shaped funnel.
SpaceABM starts where those platforms stop: it turns first-party behavior into service-lane intent and constraints, generates procurement-ready briefs, and orchestrates the next steps all the way to quotes, scheduling, and routing to fulfillable capacity—with explainable "why this is hot" logic instead of a black-box score.
The result is less vendor glue code, less brittle customization, and a system that's optimized for closing space services deals, not just running campaigns.
Full Orbit ABM — How It Works (Internal)
This document explains how the Full Orbit ABM system works end-to-end, based on what we've implemented in space-api. It covers:
- The core ABM concepts (Accounts, Contacts, Lead Requests, Signals)
- The "Request a Reservation" modal ingestion pipeline
- Lead scoring (exact rules)
- Intent signals and account rollups
- Phase 2 (6sense-lite): decayed scoring, surge detection, registry-driven architecture
- PostHog aggregation + daily batch job
- Three dashboards (Hot Accounts, Service Lane Intent, People Inside Accounts)
- AI Account Summary (cached)
- Routing/workflow state
- The API surface we've added
Overview
Full Orbit ABM is built around a simple idea:
- Prospects generate intent by interacting with Full Orbit pages and, most importantly, by submitting a Request a Reservation lead form.
- The backend stores each submission as a Lead Request (a canonical, auditable "sales artifact").
- The backend upserts the associated Prospect Company (account) and Contact (lead/person).
- The backend writes Intent Signals (a time-series explanation of "why this account is hot").
- The backend computes and maintains:
- a Lead Score (per Lead Request)
- an Intent Score (rolling 30-day account score, with Phase 2 decay + normalization)
The ABM dashboard (internal) uses these entities to show:
- Top Accounts (highest intent)
- Hot Leads (highest-scoring recent requests)
- Service Lane Intent (which service areas are trending)
- Phase 2: Hot Accounts, Service Lane Intent, and People Inside Accounts dashboards, plus AI-generated account summaries
Service Lane (Definition)
A Service Lane is a category of service interest that Full Orbit offers. It answers: "What kind of service is this account or lead interested in?"
Sources of lane assignment:
- Lead Request (explicit) — When someone submits the "Request a Reservation" widget, they select a service (e.g. Launch, Refuel, Disposal). That
service_neededvalue is the lane. - Page-view inference (event rules) — When we observe page views (e.g. via PostHog), we apply event rules to map URL paths to lanes. Example:
/services/relocation→ Orbit Transfer,/news/deorbit-as-a-service→ Disposal.
Current lanes (aligned with the widget "Service Needed" list):
- Launch
- Last-Mile Insertion (Post-Launch)
- Orbit Transfer (On-Orbit)
- Refuel
- Docking
- Upgrade
- Disposal
- Other (fallback for content that doesn't match a specific service)
Where lanes appear:
DailyAccountIntent.top_lane— primary lane for that account (highest 7d activity)DailyAccountIntent.lane_scores_7d_json,lane_scores_30d_json— score breakdown by laneLeadRequest.service_needed— explicit lane from the widget- Service Lanes dashboard — accounts grouped by lane, with hot/surging counts
Canonical Domain Normalization (Account Key)
Account Key = normalized domain (lowercase, strip protocol/path, strip www.).
Rules:
- Source preference: (1) domain from
organization_website, (2) fallback: email domain fromwork_email - Personal email domains (gmail.com, yahoo.com, outlook.com, hotmail.com, etc.) must not be used to group to an account
- If domain is personal →
account_key = null(we still store the Lead Request, but do not create a Prospect Company)
Shared helper: utils/domain.js
normalizeDomainFromUrl(url)normalizeDomainFromEmail(email)— returnsnullfor personal domainsresolveAccountKey(payload)— prefers org website, then work email (null if personal)
Examples:
https://www.AcmeSpace.com/path→acmespace.comname@gmail.com→null
Data Model (People + ABM)
1) Prospect Companies (ABM Accounts)
A Prospect Company represents an organization we are selling to.
Key properties:
domain(unique): primary identifier for the accountnamestage(ex:new,engaged,opportunity,customer)intent_score(integer): rollup score (0–100 in Phase 2)intent_last_at: last time we updated intent
Phase 2 additions:
intent_stage:Cold|Warm|Hot(from decayed scoring)surge_level:Normal|Surging|Explodingtop_lane: primary service lane (e.g.Launch,Refuel,Disposal)last_seen_at,score_updated_atscore_7d_raw,score_30d_raw(debug)salesforce_account_id,salesforce_account_url,salesforce_owner_id(future sync)
2) Contacts (People at Prospect Companies)
A Contact represents an identified person associated with a Prospect Company (usually from a lead form submission).
Key properties:
prospect_company_idemail(stored per company)status(ex:new,engaged,meeting_set,qualified)last_seen_attitle(in v1 we maprolefrom the payload intotitleiftitleis missing)
Phase 2 additions:
salesforce_lead_id,salesforce_contact_id(future sync)
3) Lead Requests (Canonical Submission Artifact)
A Lead Request is one completed "Request a Reservation" submission.
Key properties: (see original list in prior section)
Phase 2 additions:
account_key(nullable): resolved normalized domain for easier joinswhy_hot_json(optional): top 3 "why hot" reasonssalesforce_lead_id,salesforce_task_id(future sync)
4) Intent Signals (The "Why It's Hot" Timeline)
An Intent Signal represents a time-series entry that increases account intent.
Signals are stored with:
prospect_company_idsignal_type(string)topic(we useservice_neededas the topic/service-lane axis)weight(integer)occurred_at(timestamp)
5) Daily Account Intent (Phase 2)
Stores daily rollups for dashboards and account detail timeline.
prospect_company_id,datescore_config_id,raw_score_7d,raw_score_prev_7d,raw_score_30dintent_score(0–100),intent_stage,surge_ratio,surge_levelunique_people_7d,top_lanelane_scores_7d_json,lane_scores_30d_jsonkey_events_7d_json,top_categories_7d_json,top_pages_7d_json
6) Account AI Summaries (Phase 2)
Caches the "premium" AI-generated account brief.
prospect_company_id,cache_date,top_laneintent_score,surge_levelprompt_template_id,input_json,summary_md,model
7) Contact Identities (Phase 2)
Links contacts to external identifiers for the People dashboard.
contact_id,identity_type(posthog_distinct_id|email|hashed_email|crm_id)identity_value(unique per type)
8) Registry Tables (Phase 2)
Configuration-driven, no hardcoding:
- abm_event_rules: map URL patterns → lane/content_type/weight (path_prefix, contains, etc.)
- abm_score_configs: versioned scoring models (lambda_decay, normalize_k, stage thresholds, surge thresholds)
- abm_score_weights: event weights per config (page_view by content_type, cta_click by cta_id, form events)
- abm_prompt_templates: prompt per lane + persona + stage for AI summaries
Ingestion: Request a Reservation → ABM System
Endpoint
POST /api/hooks/lead-requests
Optional auth: x-lead-request-secret header if LEAD_REQUEST_SECRET is set.
Server Processing Steps
When POST /api/hooks/lead-requests is called:
-
Validate required fields (service_needed, organization_name, work_email, consent_contact)
-
Resolve account key
- Use
resolveAccountKey(payload)fromutils/domain.js - Personal domains (gmail, yahoo, etc.) →
account_key = null - No Prospect Company is created for personal domains; Lead Request is still stored
- Use
-
Compute Lead Score (details below)
-
Upsert Prospect Company (only when
account_keyis non-null)- Find or create by
domain(= account_key) - Stage bump:
new→engaged
- Find or create by
-
Upsert Contact (when company exists)
-
Create Lead Request
- Store
account_keyfor easier joins - Full
payload_json
- Store
-
Create Intent Signals
-
Recompute Account Intent Score (simple 30-day sum; Phase 2 batch job does decayed scoring separately)
Lead Scoring (Exact Logic)
Stored in lead_requests.lead_score. (See original weights: Consent, Budget Band, Funding, Urgency, Readiness, Integration, Mission Type, Completeness.)
Phase 2: Decayed Intent Scoring (6sense-lite)
The daily batch job computes a more sophisticated Account Intent Score using:
Exponential Decay
decay(age_days) = exp(-λ × age_days)(default λ = 0.10)- Contribution per event:
weight × decay(age_days)
Windows
raw_7d,raw_prev_7d,raw_30d(events in 0–7d, 7–14d, 0–30d with decay)- Lane-level:
lane_scores_7d,lane_scores_30d
Normalization
intent_score = round(100 × (1 - exp(-raw_30d / k)))(default k = 80)- Result: 0–100 score
Stage Thresholds
- Cold: 0..34
- Warm: 35..69
- Hot: 70+
Surge Classification
surge_ratio = (raw_7d + 5) / (raw_prev_7d + 5)- Normal: < 1.5
- Surging: 1.5–2.5
- Exploding: > 2.5
"Why Hot" Reasons
Top 3 from 7d key events, e.g. ["2× Pricing", "1× Security", "1× Form Started"]
Event Weights (Registry)
- page_view by content_type: pricing 25, request_reservation 30, integrations 18, security 18, case_study 12, etc.
- cta_click: request_reservation 25, contact_sales 20
- form_started 20, form_submitted 60
PostHog Aggregation + Daily Batch Job
- No raw PostHog events in MySQL — we aggregate only.
- Daily batch at 2am UTC (BullMQ job)
- Sources: PostHog API (HogQL) when configured, or
intent_signalsas fallback - For each account: compute scores, write
daily_account_intent, updateprospect_companies - Manual trigger:
POST /api/abm/jobs/recompute-intent(internal auth) - Queue/worker:
abm-intent-recompute,abmIntentWorker.js - Schedule:
npm run schedule:abm(run once on deploy)
Env: POSTHOG_HOST, POSTHOG_API_KEY, POSTHOG_PROJECT_ID, REDIS_URL
Intent Signals (What We Write, and Why)
When a Lead Request is created, we write intent signals (lead_submitted, budget_band, schedule_urgency, readiness_confidence) as described earlier.
Account Intent Score Rollup
Simple (real-time): sum of IntentSignal.weight for last 30 days (used on lead ingestion).
Phase 2 (batch): decayed scoring with normalization, stage, surge, stored in daily_account_intent and mirrored to prospect_companies.
Routing and Workflow
routing_status:new | routed | contacted | closed_won | closed_lostrouted_to_user_id,internal_notes,tags_json,disposition_reason
ABM API Routes Implemented
Public ingestion
POST /api/hooks/lead-requestsPOST /api/hooks/posthog(PostHog webhook, optional)
Internal ABM — Lead Requests
GET /api/abm/lead-requests(list, filter, paginate)GET /api/abm/lead-requests/:idPATCH /api/abm/lead-requests/:id(workflow fields)POST /api/abm/lead-requests/:id/convertGET /api/abm/lead-requests/summaryGET /api/abm/lead-requests/:id/timeline
Internal ABM — Phase 2 Dashboards
GET /api/abm/accounts— Hot Accounts (range, stage, lane, surge, search, page, limit)GET /api/abm/accounts/:id— Account detail (snapshot, timeline, people, AI summary)POST /api/abm/accounts/:id/ai-summary— Generate or return cached AI summaryGET /api/abm/lanes— Service Lane Intent (hot/surging/exploding counts per lane)GET /api/abm/people— People Inside Accounts (account_id optional)
Internal ABM — Jobs
POST /api/abm/jobs/recompute-intent— Enqueue intent recompute job
Legacy (still supported)
GET /api/abm/companies— List prospect companiesGET /api/abm/companies/:id— Company detailPOST /api/abm/companies/:id/convert
AI Account Summary
- Endpoint:
POST /api/abm/accounts/:id/ai-summary - Caching: by (account_id, cache_date, top_lane)
- Regeneration: when intent_score change ≥ 10, or surge_level changes, or
?force=true - Prompt: from
abm_prompt_templatesregistry (lane + persona + stage precedence) - Model:
AI_MODELenv (default gpt-4o-mini)
Recommended Tracking Fields (Client)
utm.*,tracking.session_id,tracking.client_id,tracking.posthog_distinct_id
Required Env Vars (Phase 2)
POSTHOG_HOST,POSTHOG_API_KEY,POSTHOG_PROJECT_IDREDIS_URL(BullMQ)OPENAI_API_KEY,AI_MODEL(AI summaries)LEAD_REQUEST_SECRET(optional)
Glossary
- Account Key: normalized domain used to resolve/group accounts (null for personal email domains)
- Contact: an identified person at a Prospect Company (usually via email)
- Content Type: category of page/event used for scoring (e.g. pricing, security, request_reservation, service_page) — from event rules or inferred
- Daily Account Intent: daily snapshot of decayed scores, stage, surge, lanes (Phase 2)
- Event Rule: registry config that maps URL patterns + event type → content_type and lane (path_prefix, contains, equals, path_regex)
- Intent Score: 0–100 normalized score from decayed event weights (Phase 2) or rolling 30-day sum (legacy)
- Intent Signal: time-series event explaining why a company is heating up
- Intent Stage: Cold | Warm | Hot (from score thresholds)
- Lead Request: one completed reservation/procurement submission (canonical record)
- Lead Score: qualification score computed from a submission (per Lead Request)
- Prompt Template: registry config for AI summaries — system/user prompt per lane + persona + intent_stage
- Prospect Company: ABM account we are selling to (domain-deduped)
- Recompute Job: daily batch (BullMQ) that fetches events, applies event rules, computes decayed scores, writes DailyAccountIntent
- Registry: configuration tables (event rules, score configs, weights, prompt templates) — no hardcoding
- Score Config: versioned scoring model (lambda_decay, normalize_k, stage/surge thresholds) — only one active at a time
- Service Lane: category of service interest (Launch, Refuel, Disposal, etc.); from
LeadRequest.service_neededor inferred from page views via event rules - Surge Level: Normal | Surging | Exploding (from 7d vs prev-7d ratio)
- Top Lane: primary service lane with highest 7d activity for an account