Capabilities & Build Guide

How OnlyData works end to end — the shared entity graph, how custom datasets get built and stay alive, what's platform vs. per-dataset, and how we co-develop it. Written as the working reference for the team.

Contents
  1. Platform capabilities at a glance
  2. One shared graph, many dataset views (the "co-op" model)
  3. Deep dive: AI Economy — anatomy of a custom dataset build
  4. Deep dive: Boise Real Estate — people + companies, custom levels
  5. Daily update processes
  6. Co-developing equally (without sharing a personal machine)
  7. Enhancements: the "set-and-forget dataset" strategy
  8. Open issues & good first digs

1. Platform capabilities at a glance

CapabilityWhat it doesWhere it lives
Canonical entity graphOne deduped record per company (keyed by domain) and per person (keyed by LinkedIn URL / normalized name). Hard invariant: one normalized domain = one active company row. Person↔company links with relationship type, primacy, and dates.od_businesses, od_profiles, od_team_links
Entity resolutionCandidate-accumulation matcher: domain exact (95), phone (85), name+address (80), fuzzy name via Dice ≥ 0.7 (50–65), city boost (+10); accept at ≥ 65. Merges are soft (loser gets is_active=false + merged_into_id) with a full reversible audit snapshot.matchRow() in server.js, od_merge_audit, dedup scripts
Enrichment engineQueue-driven, 10 models split across two workers: local Ollama box ($0 inference: industry, B2B/B2C, description, employee estimate, AI-native, agent role) and Railway (verification, MX profile, Agent Readiness). Cascade engine re-queues downstream models when an upstream field changes. Precedence: owner > admin > curated > LLM > null — curated values are never clobbered.od_enrichment_queue, two workers, triggerCascade()
ScoringCompany Agent Readiness (7-endpoint scan + bonuses for llms.txt / mcp.json / OpenAPI), Builder Tier identity floors (S/A/B), Person AR v2 (5 dimensions, archetype-aware floors). Raw score always shown alongside floored.agent_readiness_v2, compute-builder-tier.mjs, person-ar-v2.js
Living datasetsEvery dataset carries a changelog (od_dataset_history), daily metric snapshots, a field dictionary with coverage targets, and auto-flagged anomalies (row-count Δ ≥ 5% or fill Δ ≥ 3pp with no history entry = anomaly). Anchor sets provide external coverage denominators.migration 038, snapshot-datasets.mjs, check-dataset-integrity.mjs
Self-healing data quality19 axioms checked nightly against the live DB with safe auto-fix; domain de-childing (chains/products never inherit a parent's domain); membership-rot repair; category drift audits driven by per-dataset taxonomy files.data/axioms.json, daily-data-quality-check.mjs, repair SQL
Semantic layer384-dim MiniLM embeddings on companies and custom rows, pgvector search, similarity explorer.migration 039, match_businesses() / match_custom_rows()
Agent surface80+ MCP tools (remote or npx), REST API, signed webhooks in and out, llms.txt / openapi.json / mcp.json discovery endpoints.mcp.onlydata.club/mcp, /developers
Cooperative sharingOpt-in per-profile sharing with partner tools (Rally et al.) under a cooperative license — attribution required, no resale, revocable.coop_enabled, GET /api/coop/profiles

2. One shared graph, many dataset views (the "co-op" model)

"Co-op" means two related things here, and keeping them straight matters:

2a. The shared canonical graph — the architectural co-op

Datasets are filtered views, not ownership. No dataset owns its entities. Every company any dataset touches becomes (or updates) a canonical od_businesses row; every person becomes (or links to) a canonical od_profiles row. Dataset membership is a separate many-to-many table:

Why this is the whole ballgame: enrichment done for one dataset immediately benefits every other. When AI Economy queues 10 enrichment models against a new company, that company shows up scored and described in every other list it qualifies for. Every custom dataset build should pump entities into the shared graph — that's the network effect: each customer's dataset makes the next customer's dataset cheaper and better.

The rule we follow (and should harden into the platform): wherever the custom dataset's entity type allows it — companies with a real domain, people with a resolvable identity — rows are created/merged into the canonical graph and the dataset holds only a membership pointer plus its custom attributes. Only attributes that are genuinely dataset-specific (e.g. a realtor's current_listings) stay in the dataset's JSONB. Shared attributes (description, industry, employee estimate, AR score) live once, on the canonical row.

2b. The literal co-op feed — opt-in cooperative sharing

GET /api/coop/profiles returns only profiles where the owner opted in (coop_enabled), licensed cooperatively (attribution required, no resale) to partner tools like Rally. Curated person rows (e.g. Boise agents) enter this feed only when the person claims their profile and opts in — which is exactly the product hook: build the dataset → agents claim → claimed profiles join the co-op → the co-op powers every partner surface.

3. Deep dive: AI Economy — anatomy of a custom dataset build

The AI Economy list (live page) is a curated company dataset: ~26 sectors, companies stored directly in the shared graph with list_slug='ai_economy' memberships. It is the prototype for the repeatable dataset-automation template. Here is exactly what is standard platform and what is custom.

3a. Storage & identity shared

No custom tables at all. Companies are canonical od_businesses rows (source='ai_economy_curated', tier 1); sector membership is od_business_lists rows. A company can carry multiple sublists (unique on business + list + sublist + source). The public page renders straight off the membership query — one shared endpoint, GET /api/lists/:slug/companies, used by every curated list.

3b. Matching & entity resolution shared custom policy

3c. Attributes & enrichment shared custom shortcut

Every new company gets the full shared 10-model queue: canonical URL → verification → industry (→ NAICS) → MX/SPF/DKIM profile → B2B/B2C → Agent Readiness → description → AI-native → agent ecosystem role → employee estimate. One custom shortcut: sector membership deterministically implies the agent-ecosystem role (a shared sublist→role map covers five curated lists), so the LLM classifier only runs on non-curated companies (and in shadow mode on curated ones, for comparison).

3d. Cleanup processes shared custom taxonomy

3e. Metrics gap

Honest status: AI Economy has no daily metrics snapshot. Because it isn't an od_custom_datasets row, the generic snapshot and integrity jobs skip it; its only per-run metrics are the JSONL discovery log and the Telegram summary. The Boise pipeline (§4) shipped a metrics + auto-heal contract that AI Economy should adopt. This is recommendation #3 in §7.

3f. What's shared vs. custom — file inventory

Shared platformAI-Economy-specific
Match/resolvematchRow(), normalizers, dedup + merge scripts, od_merge_auditexact-domain-only ingest policy; repair-ai-economy-memberships.sql
Ingestcreate-or-match helpers, slug assignment, mega-domain blocklist patterningest-ai-economy-additions.pyadditions-only semantics: create / membership-backfill / skip, never PATCH an existing row (protects enriched + admin-edited values)
Attributes10-model queue, both workers, cascade engine, precedence rulessublist→role deterministic block; curated-precedence guard on super_category
Cleanupaxioms, nightly quality check, drift auditor, domain de-childingtaxonomy JSON, category-merge + sublist-normalize scripts
Discoverythe daily discovery skill: ranked source classes, exclusion rules (products ≠ companies; energy infra routes to the ai_power list), rotating category sweep
Metricssnapshot/history/anomaly machinery (unused here)JSONL run log only — see gap above

4. Deep dive: Boise Real Estate — people + companies, custom levels

Built 2026-08-15→16 for the JV with Andrew ("AI tools for local real estate agents"): concept to live, daily-refreshed data in ~24 hours, precisely because ~80% of it is the shared platform plus the AI Economy template. The dataset being built to demo the service is the service — it's also the JV's own prospect pipeline.

4a. Two coupled halves, deliberately not one table

HalfStorageIdentity keyPage
Companies — brokerages, teams, MLS, lenders, title, builders, RE tech (9 sectors)shared graph: od_businesses + memberships (list_slug='boise_real_estate')domainboise-real-estate
Agents — peoplecustom dataset (od_custom_rows JSONB) bridged to canonical od_profiles via profile_slugnormalized name → profile slugboise-real-estate-agents

~135 agents · ~23 companies at time of writing.

4b. Custom levels & hierarchy — how realtor / broker / team / franchise are modeled

4c. Entity resolution — loose at the data layer, strict at the graph layer

agent (od_custom_rows.row_data)
  └─ profile_slug ──► od_profiles (canonical person)
        └─ od_team_links (person ↔ company, is_primary, relationship_type)
              └─ od_businesses = the TEAM or BROKERAGE (one tier)
                    └─ od_business_lists (sublist = canonical sector)

4d. Custom attributes (the part that's genuinely per-dataset)

All 14 agent fields live in the dataset's JSONB and are documented in a field dictionary with types, enums, quality rules, and coverage targets: name, brokerage, brokerage_domain, role, specialty, service_area, years_active, website, profile_url, evidence_url, why_in_db, status, email, current_listings. Standout rules: years_active from first-party statements only (never estimated); email from first-party contact pages only (never directories); why_in_db must cite a public artifact; current_listings is the freshness signal ("N (source-url)"). No scraped socials — linkedin_url is intentionally null across the dataset. Everything shared (description, AR score, sector, domain hygiene) lives on the canonical company rows, not here.

4e. Metrics & the auto-heal contract now the template

One daily snapshot covers both halves (agents total/by-status/by-role, email + listings fill %, companies by sector, domain fill %, top brokerages by agent count, 24h deltas), written idempotently into the shared snapshot table. The contract: --check exits clean if today's snapshot exists, otherwise writes it (self-heal); the wrapper gives one retry; on final failure the script itself alerts with method + table + response body. Success also appends a one-line metrics record to the JV agenda doc so the partner sees the numbers daily.

4f. Shared vs. custom, and the co-op angle

5. Daily update processes

Both datasets refresh via the same pattern: a scheduled morning job runs an agent end-to-end through the dataset's discovery skill — the skill markdown is the program; the Python/SQL scripts are its deterministic sub-steps.

AI Economy (06:23)Boise RE (06:43)
Discover3–5 searches: funding roundups, AI trackers, YC batches, stealth launches, rotating sector sweepbrokerage roster pages, RealTrends Idaho, BoiseDev / IBR, board + MLS announcements, license lookups, rotating sweep
Dedupe + verifyexact domain vs. graph; official-site domain attestationcompanies by domain; agents by normalized name; affiliation verified
Ingestappend-only CSV → additions-only ingest (create / backfill membership / skip)same for companies; batch JSON → additions-only agent adder → dedup → embeddings → profile seeding
Healdomain de-childing → membership-rot repair (verify 0) → category merge → sublist normalize → worker liveness checkssame, plus metrics snapshot with auto-heal + one retry + self-alerting
RecordJSONL run log line → commit + push (auto-deploy) → Telegram summary. "No new companies found" is a valid outcome.
Design principles baked into both: additions-only (a daily job must never clobber enriched or admin-edited values) · never guess (domains, sublists, and names that can't be verified get skipped and logged, not invented) · always repair (rot recurs; the repair step is unconditional) · everything leaves a trail (append-only CSVs, JSONL logs, dataset history, commit history).

6. Co-developing equally (without sharing a personal machine)

Context for Jody: the local enrichment worker and the morning discovery jobs currently run on Cam's personal Mac Studio ("Stu"). That machine also holds Cam's personal context, messages, and credentials — so machine access is not the collaboration surface, and can't be. The good news: the architecture already separates "what runs on Stu" from "what Stu runs on," so equal co-development doesn't require the box.

6a. What's fully shared today

6b. The pattern: the queue is the API, the repo is the machine

Stu's only privileged role is being a poller: it claims tasks from od_enrichment_queue and runs local models against them. That means Jody can do classifications and model tweaks at full parity like this:

  1. Model/prompt changes live in the repo. The local worker's prompts, model choices, required-input contracts, and the sublist→role map are all files. Jody edits → PR → merge. Add a small "worker pulls latest main before each batch" step (recommendation below) and merged changes take effect on the next cycle with zero human-on-Stu involvement.
  2. Work is dispatched through the queue. Jody enqueues re-classification jobs (by model, by segment, by dataset) via SQL or an admin endpoint — Stu picks them up on its own schedule. He never needs a shell.
  3. Shadow mode for evaluation. The agent-role classifier already runs in shadow on curated rows. Extend that pattern: any model tweak can run shadow-first, results land in a comparison table, and promotion to live is a reviewed PR.
  4. Ollama is commodity. For interactive iteration, Jody runs the same worker script against his own Ollama (same models) with his own Supabase creds and worker='jody' — the queue's worker column already supports routing. Nothing about local inference is Stu-specific.

6c. Boundaries (explicit, so nothing is weird later)

7. Enhancements: the "set-and-forget dataset" strategy

The strategy: customers (or we) define a custom dataset once — entity types, sources, taxonomy, custom fields, cadence — then the platform rips: creates it, finds new records, enriches, auto-heals, and updates daily, using as much shared data and shared code as possible, and pumping every eligible entity into the shared graph. Ranked recommendations:

  1. Dataset definition as config. AI Economy and Boise RE are ~90% identical pipelines built by cloning ~10 files each. Collapse the template into one declarative file per dataset (datasets/<slug>.json): entity types, source classes, taxonomy ref, custom field dictionary, ingest policy (additions-only default), discovery cadence, exclusion rules. The harness generates the skill, schedules the job, and wires hygiene. This is the literal "set and forget" enabler — a new dataset becomes a config PR, not an engineering project.
  2. One parameterized hygiene suite. The repair SQL, category-merge, and sublist-normalize scripts are per-dataset clones with a hardcoded slug; the drift auditor is already generic. Parameterize the rest by list_slug + taxonomy file so every dataset gets identical healing for free and fixes land once.
  3. Metrics parity + registry. Generalize the Boise metrics/auto-heal contract into dataset-metrics.py --dataset <slug> and register list-backed datasets (like AI Economy) in the dataset registry so snapshot, integrity, history, and anomaly machinery cover everything. Today AI Economy is invisible to all four.
  4. Kill the half-merge bug class. The membership-rot engine was 52 half-finished merges (active loser holds domain, canonical holds NULL). Make merge atomic — move domain + deactivate loser in one transaction — and add an axiom: no active row's domain may belong to a row that is the merge-target of another. Then rot can't be created, not just repaired.
  5. "Promote to graph" as a platform step. Formalize the rule from §2: at custom-dataset creation and on every row add, auto-resolve rows against the canonical graph where entity type allows (company-by-domain, person-by-identity), create canonical entities for misses, and store pointers — so co-op growth is automatic, not a per-dataset convention. Custom JSONB keeps only genuinely dataset-specific fields.
  6. Brand/franchise entities. Add entity_type='brand' + parent_business_id so KW/Compass/RE-MAX (and, on the AI side, umbrella companies vs. products) become linkable parents instead of blocked strings — unlocking roll-up queries ("all agents under Compass-affiliated teams") without breaking one-domain-one-entity.
  7. Machine-independent scheduling + worker auto-update. Move the two 6am discovery agents to cloud-scheduled routines; have the local worker git pull before each batch and report its running commit into queue telemetry. Together with §6 this makes co-dev truly equal and removes the single-machine dependency.
  8. Coverage denominators per dataset. Extend anchor sets (external universes with known totals) to new datasets — for Boise: licensed-agent counts from the state board; for AI Economy: tracked-funding universes — so "how complete are we" is a real metric, which is the honest core of the set-and-forget pitch.

8. Open issues & good first digs

Real, current, and each one teaches a subsystem:

Deeper references: /developers (API + MCP tools) · /datasets (how a domain becomes a full profile) · /stats (axiom + dataset trends) · the repo's skills directory for each dataset's operational spec.