DAO -- Deal and Access Orchestrator
DAO -- Deal and Access Orchestrator
Agent: api-architect
Domain: API Design, Data Modeling, Integration Architecture
Date: 2026-04-16
Memory Search Results
- Searched: memories/agents/ for "DAO", "deal orchestrator", "network value"
- Found: No prior work on this project
- Searched: cloudflare/vira-tasks-api/ for existing Worker + KV patterns
- Found: Existing Worker pattern (API key auth, KV storage, CORS, Cloudflare Pages integration)
- Applying: Same auth pattern (X-API-Key header), same CORS approach, same deployment model
1. Overview
The DAO (Deal and Access Orchestrator) is a standalone web application that maps Rimah Harb's network across Pure Technology (PT) and MAKR Venture Fund, surfaces synergies between contacts, and tracks value creation.
Core problem: Rimah sits at the intersection of two entities with overlapping but distinct networks. Opportunities fall through cracks because no system connects "MAKR pipeline company needs X" with "PT contact has X." The DAO closes that gap.
Architecture:
- Frontend: Static SPA deployed on Cloudflare Pages
- Backend: Cloudflare Worker (Hono framework for routing)
- Database: Cloudflare D1 (SQLite)
- Cache/Fast Reads: Cloudflare KV (namespace
f3fc152d399944beb3042d7c8da59850) - Widget export: JSON endpoint consumed by Command Center
2. D1 Database Schema
2.1 Core Tables
-- ============================================================
-- CONTACTS
-- Primary table: 16K+ records imported from LinkedIn CSV
-- ============================================================
CREATE TABLE contacts (
id TEXT PRIMARY KEY, -- nanoid
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED,
email TEXT,
phone TEXT,
linkedin_url TEXT UNIQUE,
headline TEXT, -- LinkedIn headline
company TEXT, -- Current company name
position TEXT, -- Current title
location TEXT,
sector_tag TEXT, -- From LinkedIn CSV (e.g., "fintech", "healthtech")
seniority_tag TEXT, -- From LinkedIn CSV (e.g., "C-suite", "VP", "Director")
source TEXT NOT NULL DEFAULT 'linkedin', -- 'linkedin', 'makr_crm', 'manual'
entity_affinity TEXT NOT NULL DEFAULT 'both', -- 'pt', 'makr', 'both', 'external'
connection_strength INTEGER DEFAULT 3, -- 1 (weak) to 5 (strong)
last_interaction_at TEXT, -- ISO 8601
notes TEXT,
is_archived INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_contacts_sector ON contacts(sector_tag);
CREATE INDEX idx_contacts_seniority ON contacts(seniority_tag);
CREATE INDEX idx_contacts_entity ON contacts(entity_affinity);
CREATE INDEX idx_contacts_company ON contacts(company);
CREATE INDEX idx_contacts_strength ON contacts(connection_strength DESC);
CREATE INDEX idx_contacts_fullname ON contacts(full_name);
-- ============================================================
-- ENTITIES
-- Organizations: PT, MAKR, and external companies
-- ============================================================
CREATE TABLE entities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'internal' (PT/MAKR), 'portfolio', 'prospect', 'partner', 'other'
category TEXT, -- 'venture_fund', 'tech_company', 'corporate', 'startup', 'advisory'
website TEXT,
sector TEXT,
stage TEXT, -- For startups: 'pre-seed', 'seed', 'series_a', etc.
description TEXT,
is_makr_pipeline INTEGER NOT NULL DEFAULT 0,
is_pt_partner INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_entities_type ON entities(type);
CREATE INDEX idx_entities_sector ON entities(sector);
CREATE INDEX idx_entities_pipeline ON entities(is_makr_pipeline) WHERE is_makr_pipeline = 1;
-- ============================================================
-- RELATIONSHIPS
-- Contact-to-contact and contact-to-entity edges
-- ============================================================
CREATE TABLE relationships (
id TEXT PRIMARY KEY,
source_type TEXT NOT NULL, -- 'contact' or 'entity'
source_id TEXT NOT NULL,
target_type TEXT NOT NULL, -- 'contact' or 'entity'
target_id TEXT NOT NULL,
relation TEXT NOT NULL, -- 'knows', 'works_at', 'advises', 'invested_in', 'introduced_by', 'co_founder'
strength INTEGER DEFAULT 3, -- 1-5
context TEXT, -- How they know each other
via_entity TEXT, -- 'pt', 'makr', 'both', 'external'
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(source_type, source_id, target_type, target_id, relation)
);
CREATE INDEX idx_rel_source ON relationships(source_type, source_id);
CREATE INDEX idx_rel_target ON relationships(target_type, target_id);
CREATE INDEX idx_rel_via ON relationships(via_entity);
-- ============================================================
-- TAGS
-- Flexible tagging: capabilities, needs, sectors, skills
-- ============================================================
CREATE TABLE tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
category TEXT NOT NULL, -- 'capability', 'need', 'sector', 'skill', 'deal_type'
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_tags_category ON tags(category);
-- Junction table for polymorphic tagging
CREATE TABLE taggables (
tag_id TEXT NOT NULL REFERENCES tags(id),
taggable_type TEXT NOT NULL, -- 'contact', 'entity', 'opportunity'
taggable_id TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (tag_id, taggable_type, taggable_id)
);
CREATE INDEX idx_taggables_target ON taggables(taggable_type, taggable_id);
-- ============================================================
-- OPPORTUNITIES
-- Deals, partnerships, intros, collaborations
-- ============================================================
CREATE TABLE opportunities (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL, -- 'deal', 'partnership', 'intro', 'advisory', 'investment', 'tech_integration'
status TEXT NOT NULL DEFAULT 'identified', -- 'identified', 'qualifying', 'active', 'won', 'lost', 'on_hold'
routing TEXT NOT NULL, -- 'pt', 'makr', 'both'
priority TEXT NOT NULL DEFAULT 'medium',-- 'critical', 'high', 'medium', 'low'
description TEXT,
estimated_value_usd INTEGER, -- Null if not quantifiable
source_contact_id TEXT REFERENCES contacts(id),
target_entity_id TEXT REFERENCES entities(id),
synergy_id TEXT REFERENCES synergies(id), -- If born from synergy engine
closed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_opp_status ON opportunities(status);
CREATE INDEX idx_opp_routing ON opportunities(routing);
CREATE INDEX idx_opp_type ON opportunities(type);
CREATE INDEX idx_opp_priority ON opportunities(priority);
-- Participants in an opportunity
CREATE TABLE opportunity_participants (
opportunity_id TEXT NOT NULL REFERENCES opportunities(id) ON DELETE CASCADE,
participant_type TEXT NOT NULL, -- 'contact' or 'entity'
participant_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'lead', 'counterparty', 'introducer', 'advisor', 'beneficiary'
PRIMARY KEY (opportunity_id, participant_type, participant_id)
);
-- ============================================================
-- SYNERGIES
-- Engine output: detected matches between needs and capabilities
-- ============================================================
CREATE TABLE synergies (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- 'capability_match', 'sector_overlap', 'warm_intro_path', 'portfolio_fit'
score REAL NOT NULL, -- 0.0 to 1.0 confidence
status TEXT NOT NULL DEFAULT 'surfaced', -- 'surfaced', 'reviewed', 'actioned', 'dismissed'
title TEXT NOT NULL, -- Human-readable summary
description TEXT, -- Detailed reasoning
need_side_type TEXT NOT NULL, -- 'contact' or 'entity'
need_side_id TEXT NOT NULL,
supply_side_type TEXT NOT NULL, -- 'contact' or 'entity'
supply_side_id TEXT NOT NULL,
matching_tags TEXT, -- JSON array of tag IDs that matched
created_at TEXT NOT NULL DEFAULT (datetime('now')),
reviewed_at TEXT
);
CREATE INDEX idx_synergy_score ON synergies(score DESC);
CREATE INDEX idx_synergy_status ON synergies(status);
CREATE INDEX idx_synergy_type ON synergies(type);
-- ============================================================
-- ACTIVITY LOG
-- Value creation events: intros made, deals closed, revenue
-- ============================================================
CREATE TABLE activities (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- 'intro_made', 'meeting_held', 'deal_closed', 'partnership_formed',
-- 'revenue_generated', 'tech_integrated', 'investment_made', 'note'
title TEXT NOT NULL,
description TEXT,
value_usd INTEGER, -- Quantifiable value if applicable
opportunity_id TEXT REFERENCES opportunities(id),
actor_contact_id TEXT REFERENCES contacts(id),
entity_context TEXT, -- 'pt', 'makr', 'both'
metadata TEXT, -- JSON blob for type-specific data
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_activities_type ON activities(type);
CREATE INDEX idx_activities_opp ON activities(opportunity_id);
CREATE INDEX idx_activities_created ON activities(created_at DESC);
-- ============================================================
-- IMPORT TRACKING
-- Track CSV imports for deduplication and audit
-- ============================================================
CREATE TABLE imports (
id TEXT PRIMARY KEY,
source TEXT NOT NULL, -- 'linkedin_csv', 'makr_crm', 'manual_bulk'
filename TEXT,
total_rows INTEGER,
imported INTEGER,
skipped INTEGER,
errors INTEGER,
error_details TEXT, -- JSON array of row-level errors
started_at TEXT NOT NULL DEFAULT (datetime('now')),
completed_at TEXT
);
2.2 Seed Data
-- The two core entities
INSERT INTO entities (id, name, type, category, website)
VALUES
('ent_pt', 'Pure Technology', 'internal', 'tech_company', 'https://puretechnology.ai'),
('ent_makr', 'MAKR Venture Fund', 'internal', 'venture_fund', 'https://makrvf.com');
-- Core capability tags
INSERT INTO tags (id, name, category) VALUES
('tag_cap_capital', 'Capital', 'capability'),
('tag_cap_tech', 'Technology', 'capability'),
('tag_cap_distribution', 'Distribution', 'capability'),
('tag_cap_domain', 'Domain Expertise', 'capability'),
('tag_cap_talent', 'Talent Network', 'capability'),
('tag_cap_regulatory', 'Regulatory Access', 'capability'),
('tag_cap_market', 'Market Access', 'capability'),
('tag_cap_ip', 'IP/Patents', 'capability'),
('tag_need_capital', 'Needs Capital', 'need'),
('tag_need_tech', 'Needs Technology', 'need'),
('tag_need_distribution', 'Needs Distribution', 'need'),
('tag_need_domain', 'Needs Domain Expertise', 'need'),
('tag_need_talent', 'Needs Talent', 'need');
3. API Specification
Base URL: https://dao-api.<domain>.workers.dev (or custom domain)
Auth: X-API-Key header on all requests
Content-Type: application/json
CORS Origins: https://dao.pages.dev, http://localhost:3000
3.1 Common Response Envelope
All responses use a consistent envelope:
// Success
{
"ok": true,
"data": { ... },
"meta": {
"total": 16423,
"page": 1,
"per_page": 50
}
}
// Error
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "sector_tag is required",
"details": [...]
}
}
3.2 Pagination
All list endpoints accept:
?page=1(1-indexed, default 1)?per_page=50(default 50, max 200)- Response includes
meta.total,meta.page,meta.per_page
3.3 Endpoints
3.3.1 Contacts
GET /api/contacts -- List contacts with filtering
Query parameters:
| Param | Type | Description |
|---|---|---|
q | string | Full-text search on name, company, headline |
sector | string | Filter by sector_tag |
seniority | string | Filter by seniority_tag |
entity | string | Filter by entity_affinity: pt, makr, both |
company | string | Filter by company name (partial match) |
min_strength | integer | Minimum connection_strength (1-5) |
tag | string | Filter by tag name (repeatable for AND logic) |
source | string | linkedin, makr_crm, manual |
sort | string | name, company, strength, last_interaction, created |
order | string | asc or desc (default desc) |
page | integer | Page number |
per_page | integer | Results per page |
Response 200:
{
"ok": true,
"data": [
{
"id": "ct_abc123",
"first_name": "Jane",
"last_name": "Smith",
"full_name": "Jane Smith",
"email": "jane@example.com",
"linkedin_url": "https://linkedin.com/in/janesmith",
"headline": "VP Engineering at Acme Corp",
"company": "Acme Corp",
"position": "VP Engineering",
"location": "Dubai, UAE",
"sector_tag": "fintech",
"seniority_tag": "VP",
"entity_affinity": "pt",
"connection_strength": 4,
"last_interaction_at": "2026-03-15T10:00:00Z",
"tags": ["Technology", "Domain Expertise"],
"created_at": "2026-04-16T00:00:00Z"
}
],
"meta": { "total": 16423, "page": 1, "per_page": 50 }
}
GET /api/contacts/:id -- Single contact with full detail
Response 200:
{
"ok": true,
"data": {
"id": "ct_abc123",
"first_name": "Jane",
"last_name": "Smith",
"full_name": "Jane Smith",
"email": "jane@example.com",
"phone": "+971-555-1234",
"linkedin_url": "https://linkedin.com/in/janesmith",
"headline": "VP Engineering at Acme Corp",
"company": "Acme Corp",
"position": "VP Engineering",
"location": "Dubai, UAE",
"sector_tag": "fintech",
"seniority_tag": "VP",
"source": "linkedin",
"entity_affinity": "pt",
"connection_strength": 4,
"last_interaction_at": "2026-03-15T10:00:00Z",
"notes": "Met at GITEX 2025. Strong AI/ML background.",
"tags": [
{ "id": "tag_cap_tech", "name": "Technology", "category": "capability" }
],
"relationships": [
{
"id": "rel_001",
"type": "contact",
"target_id": "ct_def456",
"target_name": "Ahmed Khalil",
"relation": "knows",
"strength": 3,
"via_entity": "pt"
}
],
"opportunities": [
{
"id": "opp_xyz",
"title": "Acme x PT Data Integration",
"status": "active",
"role": "lead"
}
],
"recent_activities": [
{
"id": "act_001",
"type": "meeting_held",
"title": "Intro call with Acme team",
"created_at": "2026-03-15T10:00:00Z"
}
],
"created_at": "2026-04-16T00:00:00Z",
"updated_at": "2026-04-16T00:00:00Z"
}
}
POST /api/contacts -- Create contact
Request body:
{
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"linkedin_url": "https://linkedin.com/in/janesmith",
"company": "Acme Corp",
"position": "VP Engineering",
"sector_tag": "fintech",
"seniority_tag": "VP",
"entity_affinity": "pt",
"connection_strength": 4,
"tags": ["tag_cap_tech"]
}
Response 201: Full contact object.
PUT /api/contacts/:id -- Update contact
Request body: Partial update (only fields provided are changed).
Response 200: Updated contact object.
DELETE /api/contacts/:id -- Soft-delete (sets is_archived = 1)
Response 200:
{ "ok": true, "data": { "id": "ct_abc123", "archived": true } }
POST /api/contacts/import -- Bulk import from LinkedIn CSV
Request body: multipart/form-data
file: CSV filesource:linkedin|makr_crm(default:linkedin)default_entity_affinity:pt|makr|both(default:both)dry_run:true|false(default:false)
Response 200:
{
"ok": true,
"data": {
"import_id": "imp_001",
"total_rows": 16423,
"imported": 16201,
"skipped": 187,
"errors": 35,
"dry_run": false,
"error_sample": [
{ "row": 44, "reason": "Missing first_name" }
]
}
}
GET /api/contacts/:id/network -- Get contact's network graph (2 degrees)
Query parameters:
| Param | Type | Description |
|---|---|---|
depth | integer | 1 or 2 (default 1) |
min_strength | integer | Minimum relationship strength |
Response 200:
{
"ok": true,
"data": {
"center": { "id": "ct_abc123", "name": "Jane Smith" },
"nodes": [
{ "id": "ct_def456", "name": "Ahmed Khalil", "type": "contact", "depth": 1 },
{ "id": "ent_pt", "name": "Pure Technology", "type": "entity", "depth": 1 }
],
"edges": [
{ "source": "ct_abc123", "target": "ct_def456", "relation": "knows", "strength": 4 },
{ "source": "ct_abc123", "target": "ent_pt", "relation": "works_at", "strength": 5 }
]
}
}
3.3.2 Entities
GET /api/entities -- List entities
Query parameters: type, sector, is_makr_pipeline, is_pt_partner, q, page, per_page
Response 200: Paginated entity list.
GET /api/entities/:id -- Entity detail with contacts and opportunities
Response 200:
{
"ok": true,
"data": {
"id": "ent_ext_001",
"name": "TechCo",
"type": "prospect",
"category": "startup",
"sector": "fintech",
"stage": "series_a",
"contacts": [
{ "id": "ct_abc123", "name": "Jane Smith", "position": "VP Engineering", "relation": "works_at" }
],
"opportunities": [
{ "id": "opp_xyz", "title": "TechCo investment", "status": "qualifying", "routing": "makr" }
],
"tags": [
{ "id": "tag_need_capital", "name": "Needs Capital", "category": "need" }
]
}
}
POST /api/entities -- Create entity
PUT /api/entities/:id -- Update entity
DELETE /api/entities/:id -- Delete entity
Standard CRUD following the same patterns as contacts.
3.3.3 Relationships
POST /api/relationships -- Create relationship
Request body:
{
"source_type": "contact",
"source_id": "ct_abc123",
"target_type": "contact",
"target_id": "ct_def456",
"relation": "knows",
"strength": 4,
"context": "Met at GITEX 2025",
"via_entity": "pt"
}
Response 201: Relationship object.
PUT /api/relationships/:id -- Update relationship
DELETE /api/relationships/:id -- Remove relationship
GET /api/relationships -- List relationships with filters
Query parameters: source_id, target_id, relation, via_entity, min_strength
3.3.4 Opportunities
GET /api/opportunities -- List opportunities
Query parameters:
| Param | Type | Description |
|---|---|---|
status | string | identified, qualifying, active, won, lost, on_hold |
type | string | deal, partnership, intro, advisory, investment, tech_integration |
routing | string | pt, makr, both |
priority | string | critical, high, medium, low |
q | string | Search title/description |
sort | string | created, priority, value, status |
Response 200: Paginated list with participant summary.
GET /api/opportunities/:id -- Opportunity detail
Response 200:
{
"ok": true,
"data": {
"id": "opp_xyz",
"title": "Acme x PT Data Integration",
"type": "partnership",
"status": "active",
"routing": "pt",
"priority": "high",
"description": "Integrate Acme's fintech data pipeline with PT's AI platform.",
"estimated_value_usd": 250000,
"synergy": {
"id": "syn_001",
"type": "capability_match",
"score": 0.87
},
"participants": [
{ "type": "contact", "id": "ct_abc123", "name": "Jane Smith", "role": "lead" },
{ "type": "entity", "id": "ent_ext_001", "name": "Acme Corp", "role": "counterparty" }
],
"activities": [
{ "id": "act_001", "type": "meeting_held", "title": "Intro call", "created_at": "2026-03-15T10:00:00Z" }
],
"created_at": "2026-04-01T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
}
}
POST /api/opportunities -- Create opportunity
Request body:
{
"title": "Acme x PT Data Integration",
"type": "partnership",
"routing": "pt",
"priority": "high",
"description": "...",
"estimated_value_usd": 250000,
"participants": [
{ "type": "contact", "id": "ct_abc123", "role": "lead" },
{ "type": "entity", "id": "ent_ext_001", "role": "counterparty" }
]
}
Response 201: Full opportunity object.
PUT /api/opportunities/:id -- Update opportunity
PUT /api/opportunities/:id/status -- Transition status
Request body:
{
"status": "won",
"note": "Contract signed 2026-04-15"
}
Automatically creates an activity log entry on status transitions.
3.3.5 Synergy Engine
POST /api/synergies/scan -- Run synergy detection
Request body:
{
"scope": "all", // 'all', 'makr_pipeline', 'pt_partners', 'entity:<id>'
"min_score": 0.5, // Minimum confidence threshold
"types": ["capability_match", "sector_overlap", "warm_intro_path"],
"limit": 50
}
Response 200:
{
"ok": true,
"data": {
"scan_id": "scan_001",
"synergies_found": 23,
"synergies": [
{
"id": "syn_001",
"type": "capability_match",
"score": 0.87,
"title": "TechCo needs Capital -- Ahmed Khalil has Capital access",
"description": "TechCo (MAKR pipeline, Series A, fintech) is tagged 'Needs Capital'. Ahmed Khalil (PT network, connection strength 5) is tagged 'Capital' and has direct relationships with 3 VCs in the fintech sector.",
"need_side": { "type": "entity", "id": "ent_ext_001", "name": "TechCo" },
"supply_side": { "type": "contact", "id": "ct_def456", "name": "Ahmed Khalil" },
"matching_tags": ["Capital"],
"status": "surfaced"
}
]
}
}
GET /api/synergies -- List synergies
Query parameters: status (surfaced, reviewed, actioned, dismissed), type, min_score, sort, page, per_page
PUT /api/synergies/:id -- Update synergy status
Request body:
{
"status": "actioned",
"opportunity_id": "opp_xyz" // Link to created opportunity
}
3.3.6 Activities
GET /api/activities -- Activity feed
Query parameters: type, entity_context (pt, makr, both), opportunity_id, contact_id, since (ISO date), page, per_page
Response 200: Paginated activity list, newest first.
POST /api/activities -- Log activity
Request body:
{
"type": "intro_made",
"title": "Introduced Jane Smith to Ahmed Khalil",
"description": "Warm intro for fintech data partnership discussion.",
"opportunity_id": "opp_xyz",
"actor_contact_id": "ct_abc123",
"entity_context": "both",
"value_usd": null
}
3.3.7 Tags
GET /api/tags -- List all tags, optionally filtered by category
POST /api/tags -- Create tag
POST /api/contacts/:id/tags -- Attach tags to contact
DELETE /api/contacts/:id/tags/:tag_id -- Remove tag from contact
POST /api/entities/:id/tags -- Attach tags to entity
DELETE /api/entities/:id/tags/:tag_id -- Remove tag from entity
3.3.8 Dashboard / Widget
GET /api/dashboard/summary -- Command Center widget data
Response 200:
{
"ok": true,
"data": {
"contacts_total": 16423,
"contacts_by_entity": { "pt": 9200, "makr": 3100, "both": 4123 },
"active_opportunities": 14,
"opportunities_by_routing": { "pt": 6, "makr": 5, "both": 3 },
"pipeline_value_usd": 2350000,
"synergies_pending": 8,
"recent_activities": [
{ "id": "act_005", "type": "deal_closed", "title": "TechCo investment closed", "created_at": "2026-04-15T14:00:00Z" }
],
"top_synergies": [
{ "id": "syn_003", "title": "HealthAI needs Distribution -- PT has Distribution", "score": 0.92 }
],
"value_created_30d_usd": 500000,
"intros_made_30d": 12
}
}
This endpoint is cached in KV for 5 minutes to keep Command Center widget loads fast.
3.3.9 Search
GET /api/search -- Global search across contacts, entities, opportunities
Query parameters:
| Param | Type | Description |
|---|---|---|
q | string | Search term (required, min 2 chars) |
types | string | Comma-separated: contacts,entities,opportunities (default: all) |
limit | integer | Max results per type (default 10) |
Response 200:
{
"ok": true,
"data": {
"contacts": [
{ "id": "ct_abc123", "name": "Jane Smith", "company": "Acme Corp", "match": "name" }
],
"entities": [
{ "id": "ent_ext_001", "name": "Acme Corp", "type": "prospect", "match": "name" }
],
"opportunities": []
}
}
3.4 Error Codes
| HTTP Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request body or params |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 404 | NOT_FOUND | Resource does not exist |
| 409 | CONFLICT | Duplicate (e.g., LinkedIn URL already exists) |
| 413 | PAYLOAD_TOO_LARGE | CSV file exceeds 10MB limit |
| 422 | IMPORT_ERROR | CSV parsing failed |
| 429 | RATE_LIMITED | Too many requests (100/min) |
| 500 | INTERNAL_ERROR | Server error |
4. Synergy Matching Algorithm
4.1 Concept
The synergy engine runs four matching strategies, scores each match, and returns a ranked list. It operates entirely in D1 SQL with post-processing in the Worker.
4.2 Matching Strategies
Strategy 1: Capability Match (weight: 0.40)
Match entities/contacts tagged with "needs X" against those tagged with "has X capability."
For each entity E with tag category='need':
Find all contacts/entities with matching tag in category='capability'
Where the capability name corresponds to the need name
Score = base_weight * relationship_strength_factor * entity_affinity_bonus
Scoring factors:
- Base: 0.40
- Relationship strength: contact.connection_strength / 5 (0.2 to 1.0)
- Entity affinity bonus: +0.1 if both parties have different entity_affinity (cross-entity synergies are more valuable)
- Same sector bonus: +0.1 if sectors match
Strategy 2: Sector Overlap (weight: 0.20)
Identify MAKR pipeline companies and PT contacts operating in the same sector who are not yet connected.
For each MAKR pipeline entity E in sector S:
Find PT contacts where sector_tag = S AND connection_strength >= 3
Exclude contacts already related to E
Score = 0.20 * seniority_weight * strength_factor
Seniority weights: C-suite=1.0, VP=0.8, Director=0.6, Manager=0.4, Other=0.2
Strategy 3: Warm Intro Path (weight: 0.25)
Find two-hop paths connecting a target entity/contact to a desired contact through mutual connections.
For each opportunity O in status 'qualifying' or 'active':
Get target contacts/entities
For each target T:
Find contacts C1 where C1 knows T (from relationships)
Find contacts C2 where Rimah knows C1 with strength >= 3
Path: Rimah -> C1 -> T
Score = 0.25 * min(strength(Rimah,C1), strength(C1,T)) / 5
Strategy 4: Portfolio Fit (weight: 0.15)
Match external companies in the PT network against MAKR investment criteria (sector, stage, capabilities).
For each external entity E where is_pt_partner = 1:
Check if E.sector matches MAKR target sectors
Check if E.stage matches MAKR target stages
Check if E is NOT already in MAKR pipeline
Score = 0.15 * sector_match * stage_match * tag_overlap_count / max_tags
4.3 Composite Score
final_score = sum(strategy_scores) / count(matching_strategies)
Capped at 1.0. Synergies below min_score threshold are discarded.
4.4 Deduplication
Before returning results, deduplicate by (need_side_id, supply_side_id) pair, keeping the highest-scoring match. Also suppress synergies where an active opportunity already exists between the two parties.
5. Data Import Plan
5.1 LinkedIn CSV Format
LinkedIn exports contain these columns (mapped to contacts table):
| CSV Column | Maps To | Notes |
|---|---|---|
First Name | first_name | Required |
Last Name | last_name | Required |
Email Address | Often empty | |
Company | company | |
Position | position | |
Connected On | created_at | Parse date |
URL | linkedin_url | Unique key for dedup |
Custom columns from Rimah's tagged data:
| CSV Column | Maps To | Notes |
|---|---|---|
sector_tag | sector_tag | Already tagged by Rimah |
seniority_tag | seniority_tag | Already tagged by Rimah |
5.2 Import Process
- Upload: User uploads CSV via the import endpoint or UI.
- Parse: Worker streams CSV rows using a lightweight parser (no external deps -- Workers-compatible).
- Validate: Each row checked for required fields (first_name, last_name). Invalid rows logged to error array.
- Deduplicate: Match on
linkedin_url. If URL exists, update existing record (merge, do not overwrite non-null fields). If no URL, match on first_name + last_name + company. - Generate IDs: Each new contact gets a
ct_prefixed nanoid. - Batch Insert: Use D1 batch API (max 100 rows per batch for Worker CPU limits).
- Auto-Tag: If sector_tag or seniority_tag present, create corresponding tag relationships.
- Import Record: Write to
importstable with counts. - Invalidate Cache: Clear KV dashboard cache after import.
5.3 MAKR Contact Import
Same process but with source = 'makr_crm' and default_entity_affinity = 'makr'. If a contact exists from LinkedIn import, update entity_affinity to 'both'.
5.4 Incremental Updates
Re-importing the same CSV updates existing records rather than creating duplicates. The linkedin_url field serves as the stable identifier. The updated_at timestamp tracks when a record was last refreshed.
6. UI Wireframe Descriptions
6.1 Global Layout
- Sidebar (left, 240px, collapsed on mobile): Navigation links, entity filter toggle (PT / MAKR / All), global search bar
- Header (top, 56px): DAO logo text, current view title, user avatar placeholder
- Main content: Full remaining width, scrollable
- Design tokens: Background #000000, card background #111111, border #222222, text primary #F0F0F0, text secondary #888888, accent #D84315, font Inter
6.2 Pages
Page 1: Dashboard (Home)
The landing page. Summary metrics and actionable items.
Layout (top to bottom):
- Metric cards row (4 cards, equal width): Total Contacts, Active Opportunities, Pipeline Value (USD), Synergies Pending
- Two-column layout below:
- Left (60%): Activity Feed -- chronological list of recent activities. Each row: icon by type, title, timestamp, linked opportunity. Filter pills: All / PT / MAKR / Both.
- Right (40%): Top Synergies -- ranked list of highest-scoring unactioned synergies. Each row: score badge (color by threshold), title, need side, supply side, "Action" button (creates opportunity from synergy).
- Bottom row: Opportunity Pipeline -- horizontal kanban-style columns for Identified | Qualifying | Active | Won. Cards show title, routing badge (orange for MAKR, white for PT, split for Both), priority indicator, estimated value.
Page 2: Contacts
Filterable, searchable list of all 16K+ contacts.
Layout:
- Filter bar (top): Search input, dropdown filters for Sector, Seniority, Entity (PT/MAKR/Both), Minimum Strength (slider 1-5), Source. "Import CSV" button on right.
- Results count: "Showing 1-50 of 16,423 contacts"
- Contact table: Columns: Name (linked), Company, Position, Sector, Seniority, Entity (badge), Strength (dots), Last Interaction. Sortable columns. Row click opens contact detail.
- Pagination: Bottom of table.
Page 3: Contact Detail
Full profile for a single contact.
Layout:
- Header section: Name, headline, company, position, location, LinkedIn link (external), entity affinity badge, connection strength (editable), sector/seniority tags
- Tags section: Capability and need tags displayed as chips. "Add Tag" button opens tag picker.
- Three-tab content area:
- Relationships tab: List of connected contacts and entities. Each row: name, relation type, strength, via entity. "Add Relationship" button.
- Opportunities tab: List of opportunities this contact participates in. Status badge, role, routing.
- Activity tab: Activity log filtered to this contact.
- Sidebar panel (right, 320px): Network mini-graph showing 1-degree connections. Nodes colored by entity affinity. Click node to navigate.
Page 4: Entities
Company directory.
Layout:
- Filter bar: Search, Type dropdown, Sector dropdown, "MAKR Pipeline" toggle, "PT Partner" toggle
- Entity grid: Cards with company name, type badge, sector, stage (if startup), contact count, opportunity count. Click opens entity detail.
Page 5: Entity Detail
Layout:
- Header: Name, type, category, sector, stage, website link, description
- Tags section: Capabilities and needs as chips
- Two-column layout:
- Left: People -- contacts related to this entity, with role and relationship type
- Right: Opportunities -- deals/partnerships involving this entity
- Bottom: Related synergies involving this entity
Page 6: Opportunities
Pipeline management.
Layout:
- View toggle: List view / Kanban view
- Filter bar: Status, Type, Routing (PT/MAKR/Both), Priority, Search
- Kanban view (default): Columns for each status. Cards show title, routing badge, priority indicator, estimated value, participant avatars. Drag-and-drop to change status.
- List view: Sortable table with all opportunity fields.
- "New Opportunity" button: Opens creation form.
Page 7: Opportunity Detail
Layout:
- Header: Title, status badge (with transition dropdown), type, routing badge, priority, estimated value
- Description: Full text
- Participants section: Contacts and entities involved, each with role badge. "Add Participant" button.
- Synergy link: If born from synergy, show the original synergy card with score and reasoning.
- Activity timeline: All activities related to this opportunity, chronological.
- Actions bar: "Log Activity", "Edit", "Archive"
Page 8: Synergies
Synergy engine results.
Layout:
- Scan controls (top): Scope dropdown (All / MAKR Pipeline / PT Partners), Minimum Score slider (0.0 to 1.0), Type checkboxes, "Run Scan" button
- Results list: Sorted by score descending. Each card:
- Score badge (large, color-coded: >=0.8 green, >=0.6 amber, <0.6 gray)
- Title (e.g., "TechCo needs Capital -- Ahmed Khalil has Capital access")
- Description (reasoning)
- Need side and supply side with entity badges
- Matching tags
- Actions: "Create Opportunity" (opens pre-filled form), "Dismiss" (marks dismissed)
- Status filter pills: Surfaced / Reviewed / Actioned / Dismissed
Page 9: Settings / Import
Layout:
- Import section: File upload zone (drag-and-drop), source selector (LinkedIn / MAKR CRM), entity affinity default, dry run toggle, "Import" button
- Import history: Table of past imports with date, source, counts, status
- Tag management: List all tags by category. Add/edit/delete tags.
- API key display (masked with reveal toggle)
6.3 Navigation Sidebar
[DAO Logo]
Dashboard
Contacts (16,423)
Entities (342)
Opportunities (14)
Synergies (8 new)
Activity Log
---
Settings
Import Data
Entity filter toggle at the top of sidebar: [All] [PT] [MAKR] -- this globally filters all views.
7. Command Center Widget Integration
The Command Center fetches GET /api/dashboard/summary and renders a compact widget:
+------------------------------------------+
| DEAL & ACCESS ORCHESTRATOR |
| ----------------------------------------|
| 16,423 contacts | 14 active opps |
| $2.35M pipeline | 8 synergies pending |
| ----------------------------------------|
| Latest: TechCo investment closed 2h ago|
| Top synergy: HealthAI x PT (0.92) |
+------------------------------------------+
Widget refreshes every 5 minutes. Click-through opens DAO in a new tab.
8. Authentication and Security
- API Key: Single shared key stored in Worker environment variable. Passed via
X-API-Keyheader. Same pattern as existingvira-tasks-api. - CORS: Restricted to Pages domain and localhost.
- Rate Limiting: 100 requests/minute per IP, enforced via Worker logic.
- Input Validation: All inputs sanitized. SQL parameterized (D1 prepared statements). No raw string interpolation.
- No PII Exposure: Email and phone fields excluded from list endpoints by default. Only returned on detail endpoints.
- Import Size Limit: 10MB max CSV upload.
9. Versioning Strategy
- URL prefix:
/api/(no version in URL for v1) - When breaking changes are needed: Introduce
/api/v2/alongside/api/(which becomes v1 alias) - Deprecation: 90-day notice via response header
X-API-Deprecated: trueon old endpoints - Schema migrations: D1 migrations stored in
migrations/directory, applied via wrangler CLI
10. Implementation Phases
Phase 1 -- Foundation (MVP)
- D1 schema creation and seed data
- Contacts CRUD + CSV import
- Entities CRUD
- Tags CRUD and tagging
- Dashboard summary endpoint
- Basic UI: Dashboard, Contacts list, Contact detail, Import
Phase 2 -- Relationships and Opportunities
- Relationships CRUD
- Opportunities CRUD with status transitions
- Activity logging
- UI: Opportunities kanban, Entity pages, Activity feed
Phase 3 -- Synergy Engine
- All four matching strategies
- Synergy scan endpoint
- Synergy-to-opportunity conversion
- UI: Synergies page, network graph on contact detail
Phase 4 -- Intelligence
- Network graph visualization (2-degree)
- Global search
- Command Center widget integration
- KV caching layer for dashboard
11. File Structure
projects/dao/
DAO-SPEC.md -- This document
wrangler.toml -- Cloudflare Worker config
package.json
src/
index.ts -- Worker entry (Hono router)
routes/
contacts.ts
entities.ts
relationships.ts
opportunities.ts
synergies.ts
activities.ts
tags.ts
dashboard.ts
search.ts
import.ts
models/
contact.ts
entity.ts
relationship.ts
opportunity.ts
synergy.ts
activity.ts
tag.ts
services/
synergy-engine.ts -- Matching algorithm
csv-parser.ts -- LinkedIn CSV importer
cache.ts -- KV cache helpers
middleware/
auth.ts -- API key validation
cors.ts -- CORS headers
validation.ts -- Input validation
db/
schema.sql -- D1 schema (from section 2)
seed.sql -- Seed data
migrations/ -- Incremental migrations
frontend/
index.html
src/
app.ts
pages/
dashboard.ts
contacts.ts
contact-detail.ts
entities.ts
entity-detail.ts
opportunities.ts
opportunity-detail.ts
synergies.ts
settings.ts
components/
sidebar.ts
metric-card.ts
activity-feed.ts
synergy-card.ts
opportunity-card.ts
tag-chip.ts
network-graph.ts
import-form.ts
search-bar.ts
styles/
tokens.css -- Design tokens (#000, #111, #D84315, etc.)
global.css
lib/
api-client.ts -- Typed API wrapper
types.ts -- Shared TypeScript types
End of specification.