From dd2c34d7bcc9b90cfb9ad9b8f89a2d680a15400c Mon Sep 17 00:00:00 2001 From: Keysat Date: Fri, 5 Jun 2026 10:47:26 -0500 Subject: [PATCH] =?UTF-8?q?Phase=201:=20investor=E2=86=94contacts=20(membe?= =?UTF-8?q?r=5Fof),=20system=20status,=20thesis=20seed=20v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - entity_resolution: emit member_of relationship edges (contact -> investor), so one investor entity owns many contacts (institution) and a HNWI is the N=1 case; crm_tools.get_investor_contacts + get_entity contacts/member_of; MCP tool. - seed_synthetic: multi-contact institutions to exercise it (Harbor & Vine = 5). - server.py: GET /api/system/status (index/entity/thesis/activity health) for an in-app status view (no shell needed to verify the index). - docs/thesis-seed-v1.md: grounded v1 thesis (throughline, 6 pillars, objections, per-segment angles, voice) drawn from Ten31's newsletter/site/essays. Co-Authored-By: Claude Opus 4.8 --- backend/ingest/entity_resolution.py | 11 ++++++ backend/mcp/crm_tools.py | 24 ++++++++++++ backend/mcp/server.py | 7 ++++ backend/scripts/seed_synthetic.py | 15 ++++++++ backend/server.py | 37 ++++++++++++++++++ docs/thesis-seed-v1.md | 60 +++++++++++++++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 docs/thesis-seed-v1.md diff --git a/backend/ingest/entity_resolution.py b/backend/ingest/entity_resolution.py index 7523e4b..4690bee 100644 --- a/backend/ingest/entity_resolution.py +++ b/backend/ingest/entity_resolution.py @@ -189,6 +189,17 @@ def resolve_people(conn, org_canon_by_orgid, org_canon_by_fundinv, merge_map=Non display = full.strip() or email _upsert_entity(conn, cid, "person", display, email) _link(conn, cid, model, sid, match_value, match_kind, conf) + # Record that this contact (person) belongs to its investor/org entity, so + # one investor can own many contacts (e.g. a family office with several + # people) — and a 1-contact HNWI is just the N=1 case. + if org_canon and cid != org_canon: + conn.execute(""" + INSERT INTO relationship_edges (id, src_id, dst_id, edge_type, source, strength, directed, + first_seen_at, last_seen_at, created_at, updated_at) + VALUES (?,?,?, 'member_of', 'entity_resolution', 1.0, 1, ?, ?, ?, ?) + ON CONFLICT(src_id, dst_id, edge_type, source) + DO UPDATE SET last_seen_at=excluded.last_seen_at, updated_at=excluded.updated_at + """, (str(uuid.uuid4()), cid, org_canon, _now(), _now(), _now(), _now())) if model == "contacts": contact_to_person[sid] = cid meta = person_meta.setdefault(cid, {"org": org_canon, "last": _split_name(full)[1], diff --git a/backend/mcp/crm_tools.py b/backend/mcp/crm_tools.py index 043ecfb..4c1279e 100644 --- a/backend/mcp/crm_tools.py +++ b/backend/mcp/crm_tools.py @@ -63,10 +63,34 @@ def get_entity(lp_id, db=None): out["interaction_count"] = (c.execute( "SELECT COUNT(*) FROM communications WHERE contact_id IN (%s)" % ",".join("?" * len(cids)), list(cids)).fetchone()[0] if cids else 0) + # An investor's contacts (member_of edges) — and, for a person, the investor(s) + # they belong to. This is how one investor owns many contacts. + out["contacts"] = [dict(r) for r in c.execute( + "SELECT ce.id, ce.display_name, ce.primary_email FROM relationship_edges re " + "JOIN canonical_entities ce ON ce.id=re.src_id " + "WHERE re.dst_id=? AND re.edge_type='member_of' AND ce.deleted_at IS NULL ORDER BY ce.display_name", (lp_id,))] + out["member_of"] = [dict(r) for r in c.execute( + "SELECT ce.id, ce.display_name, ce.entity_kind FROM relationship_edges re " + "JOIN canonical_entities ce ON ce.id=re.dst_id " + "WHERE re.src_id=? AND re.edge_type='member_of' AND ce.deleted_at IS NULL", (lp_id,))] + out["contact_count"] = len(out["contacts"]) c.close() return out +def get_investor_contacts(lp_id, db=None): + """List all contacts (person entities) that belong to an investor entity — + the explicit one-investor-to-many-contacts relationship.""" + c = _conn(db) + inv = c.execute("SELECT id, entity_kind, display_name FROM canonical_entities WHERE id=?", (lp_id,)).fetchone() + contacts = [dict(r) for r in c.execute( + "SELECT ce.id, ce.display_name, ce.primary_email FROM relationship_edges re " + "JOIN canonical_entities ce ON ce.id=re.src_id " + "WHERE re.dst_id=? AND re.edge_type='member_of' AND ce.deleted_at IS NULL ORDER BY ce.display_name", (lp_id,))] + c.close() + return {"investor": dict(inv) if inv else None, "contacts": contacts, "contact_count": len(contacts)} + + def search_records(query=None, entity_kind=None, limit=20, db=None): """Structured search over canonical entities (name substring + kind).""" c = _conn(db) diff --git a/backend/mcp/server.py b/backend/mcp/server.py index a36594b..79efc9c 100644 --- a/backend/mcp/server.py +++ b/backend/mcp/server.py @@ -47,6 +47,13 @@ def get_interaction_history(lp_id: str, limit: int = 20) -> dict: return t.get_interaction_history(lp_id, limit=limit) +@mcp.tool() +def get_investor_contacts(lp_id: str) -> dict: + """List all contacts (people) belonging to an investor entity — the + one-investor-to-many-contacts relationship (e.g. a family office's several people).""" + return t.get_investor_contacts(lp_id) + + # ── retrieval modes ── @mcp.tool() def hybrid_search(query: str, top_k: int = 8, lp_id: str = "", doc_type: str = "", diff --git a/backend/scripts/seed_synthetic.py b/backend/scripts/seed_synthetic.py index 08f4274..d8e1df1 100644 --- a/backend/scripts/seed_synthetic.py +++ b/backend/scripts/seed_synthetic.py @@ -146,6 +146,21 @@ def main(): match_email = email if i % 2 == 0 else "" # half share email (easy), half don't (hard) overlap_specs.append((org_name, f"{variant} {last}", match_email)) + # Multi-contact institutions: the first two orgs get extra contacts so ONE + # investor entity owns several people (a family office / institution), to + # exercise the member_of relationship. (A HNWI stays a 1-contact investor.) + for org_name in (ORGS[0][0], ORGS[1][0]): + for k in range(2): + fn, ln = FIRST[(k + 13) % len(FIRST)], LAST[(k + 13) % len(LAST)] + cid = gen() + conn.execute( + "INSERT INTO contacts (id, first_name, last_name, email, title, organization_id, contact_type, " + "status, source, notes, created_by, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", + (cid, fn, ln, f"{fn.lower()}.{ln.lower()}@{org_name.split()[0].lower()}.invalid", + random.choice(["Analyst", "Principal", "Associate"]), org_ids[org_name], "investor", "active", + "referral", f"Additional contact at {org_name}.", uid, now())) + contacts.append((cid, fn, ln, org_name, "investor")) + # extra prospect contacts (no org sometimes) for j in range(12): first = FIRST[(j + 8) % len(FIRST)] diff --git a/backend/server.py b/backend/server.py index 6f3ca07..4f1de53 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1728,6 +1728,8 @@ class CRMHandler(BaseHTTPRequestHandler): return self.handle_get_fundraising_activity(user, params) if path == '/api/security/status': return self.handle_security_status(user) + if path == '/api/system/status': + return self.handle_system_status(user) # Users if path == '/api/users': @@ -3428,6 +3430,41 @@ class CRMHandler(BaseHTTPRequestHandler): conn.close() return self.send_json({"message": "Tag deleted"}) + def handle_system_status(self, user): + """System / search-index health for the in-app status view (DB-derived).""" + conn = get_db() + out = {} + try: + live = "deleted_at IS NULL" + out['canonical_entities'] = { + 'lp': conn.execute(f"SELECT COUNT(*) FROM canonical_entities WHERE entity_kind='lp' AND {live}").fetchone()[0], + 'organization': conn.execute(f"SELECT COUNT(*) FROM canonical_entities WHERE entity_kind='organization' AND {live}").fetchone()[0], + 'person': conn.execute(f"SELECT COUNT(*) FROM canonical_entities WHERE entity_kind='person' AND {live}").fetchone()[0], + } + out['entity_links'] = conn.execute("SELECT COUNT(*) FROM entity_links").fetchone()[0] + except Exception: + out['canonical_entities'] = None + try: + r = conn.execute("SELECT ts, payload FROM interaction_log WHERE action='ingest.sync' ORDER BY ts DESC LIMIT 1").fetchone() + out['last_index_sync'] = ({'ts': r['ts'], **json.loads(r['payload'] or '{}')} if r else None) + except Exception: + out['last_index_sync'] = None + try: + out['thesis'] = { + 'lines': conn.execute("SELECT COUNT(*) FROM thesis_lines WHERE deleted_at IS NULL").fetchone()[0], + 'canonical_versions': conn.execute("SELECT COUNT(*) FROM thesis_versions WHERE status='canonical'").fetchone()[0], + 'in_review': conn.execute("SELECT COUNT(*) FROM thesis_versions WHERE status='in_review'").fetchone()[0], + } + except Exception: + out['thesis'] = None + try: + out['recent_activity'] = [dict(r) for r in conn.execute( + "SELECT ts, actor_type, actor_id, action FROM interaction_log ORDER BY ts DESC LIMIT 12")] + except Exception: + out['recent_activity'] = [] + conn.close() + self.send_json({"data": out}) + # ─── Architect thesis (Phase 1) ─── def handle_list_thesis_lines(self, user): if thesis_review is None: diff --git a/docs/thesis-seed-v1.md b/docs/thesis-seed-v1.md new file mode 100644 index 0000000..94af9bc --- /dev/null +++ b/docs/thesis-seed-v1.md @@ -0,0 +1,60 @@ +# Ten31 Thesis — Seed v1 (a starting point to sharpen) + +*Drafted from Ten31's own material: the newsletter, ten31.xyz/home and /insights (esp. "Digital Industrialization: Land, Labor & Capital," "Credible Finance," "Coherence in an Age of Abundance"), the current self-description, and the partner's workshop notes. This is **v0 to react to**, not a finished thesis — it's the raw clay the Architect helps you and your partner sharpen (vary framings, red-team LP objections, ground each claim in your essays/track record). Where the partner's framing was "too philosophical," I've paired each abstract move with the concrete investment logic underneath it.* + +--- + +## Throughline (the spine) + +> **Every wave of digital industrialization makes one resource abundant and forces value into whatever stays scarce.** The internet made *distribution* free and pooled value in attention. AI is making *competence* free and pooling value into its scarce physical inputs — energy and compute. Bitcoin makes *capital itself* verifiable, and the last scarce resource becomes **credibility**. The pattern doesn't change: when a resource turns abundant, value abandons it for whatever stays scarce. **Ten31 invests in the scarce side of each shift — bitcoin, energy, and the credible infrastructure of the AI era — and backs the bitcoin-native founders organizing capital and production around it, while the position is still mispriced.** + +*(One-line version for cold contexts: "The world's leading investor at the convergence of bitcoin, energy, and AI — backing the scarce side of every digital shift, and the founders building it.")* + +## Pillars + +**1. The scarcity shift is the master pattern — and it's accelerating.** +Each digital epoch decentralizes one domain and concentrates value in another (your *Digital Industrialization* essay). Internet → distribution. AI → competence. Bitcoin → capital access. Value flees the newly-abundant for whatever stays scarce. *Investment logic: we position on the scarce side of the current shift — energy, compute, and verifiable capital — before the market reprices it.* + +**2. Bitcoin is the base layer of a credible economy.** +"Proof over promise." As AI floods the world with cheap, unverifiable output, verifiable scarcity and credibility become the premium — and bitcoin is the only money that delivers both (*Credible Finance*; *The Global Capital Circuit*). *Investment logic: bitcoin infrastructure — custody, payments, lending, computing, security — is the rails of that economy, and we've backed it since 2013.* + +**3. The bitcoin–energy–AI convergence is physical, not theoretical.** +Mining monetizes stranded energy; compute and bitcoin compete for the same low-cost megawatts; sovereign infrastructure and secure inference become moats as competence commoditizes (*Coherence in an Age of Abundance*). *Investment logic: energy and compute are the scarce inputs of the AI era; bitcoin prices, secures, and settles them. We invest across that physical stack (e.g. Giga Energy, Upstream Data) — "the world's largest investor focused on the convergence of bitcoin, energy, and AI."* + +**4. We back a specific kind of founder.** +Bitcoin-native operators: long time horizons, real ownership, no patience for fragility or shortcuts — people who saw the future before the market thought it possible. We don't screen for bitcoin; the best founders in this space already think this way. *Proof: Strike (global bitcoin financial services) and Start9 (personal datacenters for edge AI) share nothing as businesses — but the same kind of founder is at the top.* + +**5. The portfolio is a synergistic ecosystem, not a list of bets.** +Sequoia-inspired: complementary companies whose value compounds in combination — custody enables payments, payments drive commerce, commerce produces sovereignty (*An Investment Platform for Bitcoin Adoption*). *Proof: 35+ portfolio companies, $200M+ deployed across two funds, unmatched founder access and credibility from a decade in the ecosystem.* + +**6. Patient, credible capital — and the position is still mispriced.** +Long time preference, durability over paper markups, "gradually, then suddenly." *Investment logic: Fund III takes the convergence position while the market still underprices it.* + +## Key objections to pressure-test (red-team targets) + +- *"Isn't this just a bitcoin fund?"* → No: it's a thesis on the scarce inputs of the AI/energy era, with bitcoin as the credibility/settlement layer — a broader and more durable position than "buy bitcoin." +- *"Energy + AI infrastructure is crowded and capital-intensive."* → Our edge is founder access and the bitcoin-native lens that generalist energy/AI capital lacks; we back the operators, not the hype cycle. +- *"Concentration / correlation risk to bitcoin's price."* → The ecosystem spans infrastructure with real revenue (energy, compute, financial services), not directional bitcoin beta; durability over markups. +- *"Why now?"* → The convergence just turned physical (AI's energy/compute crunch) while bitcoin's role as credible capital is still underpriced — the gap is the opportunity. + +## Segment angles (per-segment lines, sharing the spine) + +*Per the decision that different audiences may carry different—but related—narratives:* +- **Bitcoin-native HNWI** — conviction, sovereignty, asymmetric upside; "you already see it; we invest in it." (Lean into *Because We're Right*.) +- **Institution** — risk-adjusted, credible exposure to the AI-energy buildout via a differentiated manager with a decade-long edge; secular tailwinds, durability. +- **Family office (bitcoin/AI-curious, other focuses)** — a generational allocation to the convergence; patient capital aligned with multi-generational horizons; ecosystem diversification. +- **Smaller accredited ($100k checks)** — accessible entry to the same thesis the HNWIs back; aligned conviction, lower minimum. +- **AI-oriented investors** — the scarce-inputs (energy/compute) + secure-inference angle; bitcoin as AI's settlement and credibility layer. +- **Energy players** — stranded/curtailed energy monetization; bitcoin + compute as flexible offtake; the physical buildout. + +## Voice (to encode in the `ten31-voice` skill) + +Sophisticated, macro-literate, conviction-driven, and willing to be irreverent (per the newsletter). Proof over promise; historical analogies (railroads, "gradually then suddenly"); confident but not hype-y; never crypto-casino framing (no NFTs/meme-coin energy). "This is us": James J. Hill, scarcity, credibility, sovereignty, long time preference. "Not us": growth-at-all-costs, paper markups, shiny speculation. + +--- + +## How to use this + +This becomes the seeded **core thesis line** in the Architect. From here the loop is: you and your partner react → the Architect proposes sharper variations, drafts the per-segment cuts, anticipates LP objections, and grounds each claim against your essays + real LP conversations → you both leave feedback and sign off. Nothing here is canonical until you approve it. + +**Open spots needing your input:** which pillars are load-bearing vs. cuttable; the exact Fund III framing/numbers to use externally; confirming the segment set; and any sacred phrases / forbidden framings for the voice guide.