From c903f71221d3da8b4b54a0093c13e11fb9804c62 Mon Sep 17 00:00:00 2001 From: Troy Harvey Date: Mon, 30 Mar 2026 22:49:58 -0400 Subject: [PATCH] Add carta-fund-admin plugin v0.1.0 - Fund admin skills (explore-data, form-adv, performance-benchmarks) - MCP integration for fund metrics, regulatory reporting, and benchmarks - SessionStart hook for skill-first workflow - PostToolUse hook for empty query warnings --- .claude-plugin/marketplace.json | 6 + .../.claude-plugin/plugin.json | 19 ++ plugins/carta-fund-admin/README.md | 47 ++++ plugins/carta-fund-admin/hooks/hooks.json | 27 ++ .../scripts/hooks/inject-skill-context.js | 29 ++ .../scripts/hooks/warn-empty-query.js | 56 ++++ .../skills/explore-data/SKILL.md | 101 +++++++ .../carta-fund-admin/skills/form-adv/SKILL.md | 262 ++++++++++++++++++ .../skills/performance-benchmarks/SKILL.md | 148 ++++++++++ 9 files changed, 695 insertions(+) create mode 100644 plugins/carta-fund-admin/.claude-plugin/plugin.json create mode 100644 plugins/carta-fund-admin/README.md create mode 100644 plugins/carta-fund-admin/hooks/hooks.json create mode 100644 plugins/carta-fund-admin/scripts/hooks/inject-skill-context.js create mode 100644 plugins/carta-fund-admin/scripts/hooks/warn-empty-query.js create mode 100644 plugins/carta-fund-admin/skills/explore-data/SKILL.md create mode 100644 plugins/carta-fund-admin/skills/form-adv/SKILL.md create mode 100644 plugins/carta-fund-admin/skills/performance-benchmarks/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bcc67c6..23aee7d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,6 +12,12 @@ "source": "./plugins/carta-cap-table", "description": "Carta Cap Table plugin — skills and hooks for querying cap tables, grants, SAFEs, 409A valuations, waterfall scenarios, and more", "category": "productivity" + }, + { + "name": "carta-fund-admin", + "source": "./plugins/carta-fund-admin", + "description": "Carta Fund Admin plugin — skills for querying fund admin data, performance benchmarks, regulatory reporting, and more", + "category": "productivity" } ] } diff --git a/plugins/carta-fund-admin/.claude-plugin/plugin.json b/plugins/carta-fund-admin/.claude-plugin/plugin.json new file mode 100644 index 0000000..6e4728b --- /dev/null +++ b/plugins/carta-fund-admin/.claude-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "carta-fund-admin", + "displayName": "Carta Fund Admin", + "version": "0.1.0", + "description": "Carta Fund Admin plugin — skills for querying fund admin data, performance benchmarks, regulatory reporting, and more", + "author": { + "name": "Carta Engineering", + "email": "engineering@carta.com" + }, + "homepage": "https://github.com/carta/plugins", + "repository": "https://github.com/carta/plugins", + "keywords": ["fund_admin", "mcp", "carta"], + "mcpServers": { + "carta": { + "type": "http", + "url": "https://mcp.app.carta.com/mcp" + } + } +} diff --git a/plugins/carta-fund-admin/README.md b/plugins/carta-fund-admin/README.md new file mode 100644 index 0000000..884092e --- /dev/null +++ b/plugins/carta-fund-admin/README.md @@ -0,0 +1,47 @@ +# Carta Fund Admin + +Claude Code plugin that gives Claude access to Carta Fund Admin data. + +## How it works + +This plugin provides **skills** that teach Claude how to query fund metrics, regulatory reporting, performance benchmarks, and more via the Carta MCP server at `https://mcp.app.carta.com/mcp`. + +## Installation + +Install from the marketplace via `/plugin`, or add the Carta MCP server manually: + +```bash +claude mcp add --transport http carta https://mcp.app.carta.com/mcp +claude plugin marketplace add carta/plugins +claude plugin install carta-fund-admin +``` + +After installing, restart Claude Code and run `/mcp` to complete OAuth authentication. + +### Try it out + +- "What datasets are available in Carta?" +- "Show me NAV and TVPI for all my funds" +- "Pull our Form ADV data for 2025" +- "How does Fund I compare to its benchmark?" +- "What journal entries were posted last quarter?" + +## Skills + +| Skill | Description | +|-------|-------------| +| `explore-data` | Query and explore fund admin data — NAV, partners, investments, accounting | +| `form-adv` | Form ADV Schedule D regulatory data and firm rollup | +| `performance-benchmarks` | Compare fund performance against peer benchmark cohorts | + +## MCP Tools + +The Carta MCP server exposes these data warehouse tools: + +| Tool | Description | +|------|-------------| +| `list_tables` | Browse available datasets with descriptions and record counts | +| `describe_table` | Get column names, types, and descriptions for a specific table | +| `execute_query` | Run a read-only SELECT query against the data warehouse | +| `list_contexts` | See which firms you have access to | +| `set_context` | Switch to a different firm | diff --git a/plugins/carta-fund-admin/hooks/hooks.json b/plugins/carta-fund-admin/hooks/hooks.json new file mode 100644 index 0000000..1c578f4 --- /dev/null +++ b/plugins/carta-fund-admin/hooks/hooks.json @@ -0,0 +1,27 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/inject-skill-context.js", + "timeout": 5 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "mcp__Carta__execute_query", + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/warn-empty-query.js", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/plugins/carta-fund-admin/scripts/hooks/inject-skill-context.js b/plugins/carta-fund-admin/scripts/hooks/inject-skill-context.js new file mode 100644 index 0000000..dc7f1ab --- /dev/null +++ b/plugins/carta-fund-admin/scripts/hooks/inject-skill-context.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node +/** + * SessionStart hook: inject skill-first reminder into every session. + * + * Ensures Claude loads the relevant carta-fund-admin skill before making + * any tool calls, even in subagents that don't inherit session context. + */ + +let inputData = ''; +process.stdin.on('data', chunk => (inputData += chunk)); + +process.stdin.on('end', () => { + let hookEventName = 'SessionStart'; + try { + const input = JSON.parse(inputData); + hookEventName = input.hook_event_name || hookEventName; + } catch {} + + const output = { + hookSpecificOutput: { + hookEventName, + additionalContext: + 'You have carta-fund-admin tools available via the Carta MCP server (list_tables, describe_table, execute_query). Before ANY tool call, invoke the matching Skill(\'carta-fund-admin:...\') first. The skill defines what to query, what inputs are required, and how to present results. If no skill matches the user\'s request, use list_tables to browse available datasets and describe_table to understand schemas. IMPORTANT: Skill is a deferred tool — if its schema is not yet loaded, you MUST call ToolSearch with query "select:Skill" first, then invoke the Skill tool.', + }, + }; + + process.stdout.write(JSON.stringify(output)); + process.exit(0); +}); diff --git a/plugins/carta-fund-admin/scripts/hooks/warn-empty-query.js b/plugins/carta-fund-admin/scripts/hooks/warn-empty-query.js new file mode 100644 index 0000000..b9c0fee --- /dev/null +++ b/plugins/carta-fund-admin/scripts/hooks/warn-empty-query.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/** + * PostToolUse Hook: Warn on empty execute_query responses + * + * When execute_query returns no rows, outputs a reminder to stderr + * (exit 2) which gets fed to Claude. + */ + +let inputData = ''; +process.stdin.on('data', chunk => (inputData += chunk)); + +process.stdin.on('end', () => { + try { + const input = JSON.parse(inputData); + const { tool_input, tool_response } = input; + + // Extract the result string from the MCP response + let resultStr = tool_response?.result || tool_response; + if (Array.isArray(resultStr) && resultStr[0]?.type === 'text') { + resultStr = resultStr[0].text; + } else if (resultStr?.content && Array.isArray(resultStr.content)) { + resultStr = resultStr.content[0]?.text || resultStr; + } + + // Parse and check for empty results + let parsed; + try { + parsed = typeof resultStr === 'string' ? JSON.parse(resultStr) : resultStr; + } catch { + process.exit(0); + return; + } + + const isEmpty = + (parsed && parsed._warning) || + (Array.isArray(parsed) && parsed.length === 0) || + (parsed?.rows && parsed.rows.length === 0) || + (parsed?.count === 0); + + if (isEmpty) { + const sql = tool_input?.sql || 'query'; + process.stderr.write( + `⚠️ EMPTY DATA: execute_query returned no results. ` + + `Tell the user what data was expected but missing and suggest next steps ` + + `(check firm context with list_contexts, verify table names with list_tables, etc.).` + ); + process.exit(2); + return; + } + + process.exit(0); + } catch (err) { + // Never block on hook errors + process.exit(0); + } +}); diff --git a/plugins/carta-fund-admin/skills/explore-data/SKILL.md b/plugins/carta-fund-admin/skills/explore-data/SKILL.md new file mode 100644 index 0000000..21405bb --- /dev/null +++ b/plugins/carta-fund-admin/skills/explore-data/SKILL.md @@ -0,0 +1,101 @@ +--- +name: explore-data +description: Query and explore fund admin data in the Carta data warehouse. Use when asked about fund metrics, NAV, LP data, portfolio financials, journal entries, investments, or any general data warehouse query. +--- + +# Explore Data + +Query the Carta data warehouse for fund admin data — NAV, partner data, portfolio financials, journal entries, investments, and more. + +## When to Use + +- "What's the current NAV for [Fund]?" +- "Show me TVPI for all funds" +- "List all LP investors in [Fund] with their contributions" +- "What journal entries were posted for [Fund] last quarter?" +- "Which portfolio companies have the highest MOIC?" +- "Show me total contributions and distributions for each LP" +- "What are the fund metrics as of Q4 2024?" + +## Prerequisites + +The user must have the Carta MCP server connected. If this is their first query in the session: + +1. Call `list_contexts` to see which firms are accessible +2. Call `set_context` with the target `firm_id` if needed + +## How to Query + +Use the three MCP tools in sequence: + +1. **Find the right table:** `list_tables(schema="FUND_ADMIN")` — browse available datasets +2. **Understand the schema:** `describe_table(table_name="", schema="FUND_ADMIN")` — get column details +3. **Run the query:** `execute_query(sql="SELECT ... FROM FUND_ADMIN.
WHERE ... LIMIT 1000")` — fetch results + +## Common Datasets + +| Table | Use For | +|-------|---------| +| `MONTHLY_NAV_CALCULATIONS` | NAV, commitments, distributions, DPI/TVPI/MOIC per fund per month | +| `AGGREGATE_FUND_METRICS` | LP/GP investor counts, fund-level summary metrics | +| `AGGREGATE_INVESTMENTS` | Portfolio company list, active investments, cost basis, FMV | +| `JOURNAL_ENTRIES` | Balance sheet data — cash, cost of investment, unrealized G/L, liabilities | +| `ALLOCATIONS` | Fund list with entity types (Fund, SPV), fund names, firm info | +| `TEMPORAL_FUND_COHORT_BENCHMARKS` | Performance benchmarks by vintage year, AUM bucket, percentiles | + +## Query Guidelines + +- **Always include LIMIT** — default to `LIMIT 1000` unless the user asks for more +- **Only SELECT** — no INSERT, UPDATE, DELETE, or DDL. The MCP validates this server-side +- **Date fields** — use `effective_date` for accounting dates, `month_end_date` for NAV periods +- **Deduplication** — some tables have multiple rows per entity; use `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY last_refreshed_at DESC) = 1` when needed + +## Presentation + +1. **Lead with a summary** — "Your firm has 5 funds with a combined NAV of $X" +2. **Format as tables** — use markdown tables with clear column headers +3. **Format currency** — use `$X,XXX` for amounts, `X.XXx` for multiples +4. **Flag notable items** — low NAV, negative performance, missing data +5. **Use Carta voice** — say "your funds" not "query results"; say "your NAV" not "MONTHLY_NAV_CALCULATIONS data" + +## Example: Fund NAV Summary + +```sql +SELECT + n.fund_name, + n.month_end_date, + n.ending_total_nav, + n.total_tvpi, + n.total_dpi, + n.total_moic +FROM FUND_ADMIN.MONTHLY_NAV_CALCULATIONS n +WHERE n.is_firm_rollup = FALSE +QUALIFY ROW_NUMBER() OVER (PARTITION BY n.fund_uuid ORDER BY n.month_end_date DESC, n.last_refreshed_at DESC) = 1 +ORDER BY n.ending_total_nav DESC +LIMIT 50 +``` + +| Fund | As Of | NAV | TVPI | DPI | MOIC | +|------|-------|-----|------|-----|------| +| Fund I | 2024-12-31 | $150,000,000 | 1.85x | 0.42x | 1.85x | + +## Example: LP Contributions by Fund + +```sql +SELECT + fund_name, + SUM(cumulative_lp_contributions) AS total_lp_contributions, + SUM(cumulative_total_distributions) AS total_distributions, + COUNT(DISTINCT fund_uuid) AS fund_count +FROM FUND_ADMIN.MONTHLY_NAV_CALCULATIONS +WHERE is_firm_rollup = FALSE + AND month_end_date = (SELECT MAX(month_end_date) FROM FUND_ADMIN.MONTHLY_NAV_CALCULATIONS) +GROUP BY fund_name +ORDER BY total_lp_contributions DESC +LIMIT 50 +``` + +## Best Effort + +- **Computed:** summaries, aggregations, and trend analysis derived by Claude from query results +- **Authoritative:** raw data values returned directly from the Carta data warehouse diff --git a/plugins/carta-fund-admin/skills/form-adv/SKILL.md b/plugins/carta-fund-admin/skills/form-adv/SKILL.md new file mode 100644 index 0000000..5457c62 --- /dev/null +++ b/plugins/carta-fund-admin/skills/form-adv/SKILL.md @@ -0,0 +1,262 @@ +--- +name: form-adv +description: Fetch Form ADV Schedule D regulatory data and firm rollup for fund-level reporting. Use when asked about Form ADV, regulatory AUM, Schedule D, Form PF Section 1, or annual filing data. +--- + +# Form ADV Schedule D Data + +Fetch Form ADV Schedule D §7.B.(1) regulatory data for each Fund and SPV, plus an Item 5.D. firm-level rollup. + +## When to Use + +- "Pull our Form ADV data for 2025" +- "What's our regulatory AUM as of December 2024?" +- "Show me Schedule D data for last year's filing" +- "What do I need for Form PF Section 1?" +- "What's our total AUM across all funds?" + +## Prerequisites + +- The user must have the Carta MCP server connected +- A `reporting_date` is required (YYYY-MM-DD format, e.g. "2025-12-31") — **ask if not provided** +- If this is the first query, call `list_contexts` and `set_context` to set the firm + +## Data Retrieval + +Run a single query using `execute_query` that joins across five datasets to produce the full Form ADV view. The query produces two sections: per-fund detail and a firm rollup. + +### Source Tables + +| Table | Purpose | +|-------|---------| +| `FUND_ADMIN.ALLOCATIONS` | Fund list — Fund and SPV entity types only | +| `FUND_ADMIN.JOURNAL_ENTRIES` | Balance sheet: cash, cost of investment, unrealized G/L, other assets, LOC | +| `FUND_ADMIN.MONTHLY_NAV_CALCULATIONS` | NAV, commitments, distributions (inception + annual), DPI/TVPI/MOIC | +| `FUND_ADMIN.AGGREGATE_FUND_METRICS` | LP/GP investor counts (current snapshot) | +| `FUND_ADMIN.AGGREGATE_INVESTMENTS` | Active portfolio company count | + +### Key Fields + +| Output Field | Source | Notes | +|---|---|---| +| Fair Market Value | Journal Entries | cost_of_investment + unrealized_gl | +| Cash | Journal Entries | Account types 1000–1099 | +| Other Assets | Journal Entries | Account types 1200–1899 | +| Total Gross Assets | Computed | FMV + Cash + Other Assets | +| LOC / Borrowings | Journal Entries | Accounts 2000 + 2001, negated (liabilities are credit) | +| Unfunded Commitments | NAV Calculations | GREATEST(commitment - contributions, 0) | +| Regulatory AUM | Computed | Total Gross Assets + Unfunded Commitments | +| Net Asset Value | NAV Calculations | ending_total_nav | +| Net AUM (Form PF) | Computed | NAV + Unfunded Commitments | +| Annual Subscriptions | NAV Calculations | SUM of monthly contributions in reporting year | +| Annual Distributions | NAV Calculations | SUM of monthly distributions in reporting year | +| LP/GP Investor Counts | Aggregate Fund Metrics | Current snapshot, latest row per fund | +| Portfolio Company Count | Aggregate Investments | Active investments only | +| DPI / TVPI / MOIC | NAV Calculations | At reporting date | + +### Important: Date Field + +Use `effective_date` (accounting date) from journal entries. Do NOT filter on `posted_date` — it can drop valid entries and cause mismatches against the Carta balance sheet. + +### SQL Query + +Execute this query with `execute_query`, substituting the user's reporting date: + +```sql +WITH +constants AS ( + SELECT LAST_DAY('{reporting_date}'::DATE) AS reporting_date +), +funds AS ( + SELECT + a.fund_uuid, + MAX(a.fund_name) AS fund_name, + MAX(a.entity_type_name) AS entity_type_name, + MAX(a.firm_name) AS firm_name, + c.reporting_date + FROM FUND_ADMIN.ALLOCATIONS a + CROSS JOIN constants c + WHERE a.entity_type_name IN ('Fund', 'SPV') + GROUP BY a.fund_uuid, c.reporting_date +), +je_balances AS ( + SELECT + j.fund_uuid, + SUM(CASE WHEN j.account_type BETWEEN 1000 AND 1099 THEN j.amount ELSE 0 END) AS cash, + SUM(CASE WHEN j.account_type = 1100 THEN j.amount ELSE 0 END) AS cost_of_investment, + SUM(CASE WHEN j.account_type = 1101 THEN j.amount ELSE 0 END) AS unrealized_gl, + SUM(CASE WHEN j.account_type BETWEEN 1200 AND 1899 THEN j.amount ELSE 0 END) AS other_assets, + -SUM(CASE WHEN j.account_type IN (2000, 2001) THEN j.amount ELSE 0 END) AS loc_outstanding + FROM FUND_ADMIN.JOURNAL_ENTRIES j + INNER JOIN funds f ON j.fund_uuid = f.fund_uuid + WHERE j.effective_date <= f.reporting_date + GROUP BY j.fund_uuid +), +nav_data AS ( + SELECT + n.fund_uuid, + n.ending_total_nav, + n.ending_lp_nav, + n.ending_gp_nav, + n.cumulative_commitment_amount, + n.cumulative_total_contributions, + n.cumulative_lp_contributions, + n.cumulative_gp_contributions, + n.cumulative_total_distributions, + GREATEST(n.cumulative_commitment_amount - n.cumulative_total_contributions, 0) AS unfunded_commitments, + n.total_tvpi, + n.total_moic, + n.total_dpi + FROM FUND_ADMIN.MONTHLY_NAV_CALCULATIONS n + INNER JOIN funds f ON n.fund_uuid = f.fund_uuid + WHERE n.is_firm_rollup = FALSE + AND n.month_end_date = f.reporting_date + QUALIFY ROW_NUMBER() OVER (PARTITION BY n.fund_uuid ORDER BY n.last_refreshed_at DESC) = 1 +), +annual_activity AS ( + SELECT + n.fund_uuid, + SUM(n.total_contributions) AS annual_subscriptions, + SUM(n.total_distributions) AS annual_distributions + FROM FUND_ADMIN.MONTHLY_NAV_CALCULATIONS n + INNER JOIN funds f ON n.fund_uuid = f.fund_uuid + WHERE n.is_firm_rollup = FALSE + AND n.month_end_date BETWEEN DATE_TRUNC('year', f.reporting_date) AND f.reporting_date + GROUP BY n.fund_uuid +), +lp_counts AS ( + SELECT + m.fund_uuid, + m.count_lps, + m.count_gps + FROM FUND_ADMIN.AGGREGATE_FUND_METRICS m + INNER JOIN funds f ON m.fund_uuid = f.fund_uuid + QUALIFY ROW_NUMBER() OVER (PARTITION BY m.fund_uuid ORDER BY m.last_refreshed_at DESC) = 1 +), +portfolio_summary AS ( + SELECT + ai.fund_uuid, + COUNT(DISTINCT ai.issuer_name) AS active_portfolio_companies + FROM FUND_ADMIN.AGGREGATE_INVESTMENTS ai + INNER JOIN funds f ON ai.fund_uuid = f.fund_uuid + WHERE ai.is_active_investment = TRUE + GROUP BY ai.fund_uuid +) + +SELECT + '7.B.(1) Fund Detail' AS form_adv_section, + f.fund_name, + f.entity_type_name AS entity_type, + ROUND(je.cost_of_investment + je.unrealized_gl, 2) AS fair_market_value, + ROUND(je.cash, 2) AS cash, + ROUND(je.other_assets, 2) AS other_assets, + ROUND(je.cost_of_investment + je.unrealized_gl + je.cash + je.other_assets, 2) AS total_gross_assets, + ROUND(je.loc_outstanding, 2) AS loc_borrowings_outstanding, + ROUND(nav.unfunded_commitments, 2) AS unfunded_commitments, + ROUND(je.cost_of_investment + je.unrealized_gl + je.cash + je.other_assets + nav.unfunded_commitments, 2) AS regulatory_aum, + ROUND(nav.ending_total_nav, 2) AS net_asset_value, + ROUND(nav.ending_lp_nav, 2) AS lp_nav, + ROUND(nav.ending_gp_nav, 2) AS gp_nav, + ROUND(nav.ending_total_nav + nav.unfunded_commitments, 2) AS net_aum_form_pf, + ROUND(nav.cumulative_commitment_amount, 2) AS total_committed_capital, + ROUND(act.annual_subscriptions, 2) AS annual_subscriptions, + ROUND(act.annual_distributions, 2) AS annual_distributions, + ROUND(nav.cumulative_total_distributions, 2) AS total_distributions_since_inception, + lp.count_lps AS number_of_lp_investors, + lp.count_gps AS number_of_gp_investors, + ps.active_portfolio_companies, + ROUND(nav.total_dpi, 4) AS dpi, + ROUND(nav.total_tvpi, 4) AS tvpi, + ROUND(nav.total_moic, 4) AS moic +FROM funds f +LEFT JOIN je_balances je ON f.fund_uuid = je.fund_uuid +LEFT JOIN nav_data nav ON f.fund_uuid = nav.fund_uuid +LEFT JOIN annual_activity act ON f.fund_uuid = act.fund_uuid +LEFT JOIN lp_counts lp ON f.fund_uuid = lp.fund_uuid +LEFT JOIN portfolio_summary ps ON f.fund_uuid = ps.fund_uuid + +UNION ALL + +SELECT + '5.D. Firm Rollup', + MAX(f.firm_name), + 'ALL FUNDS', + ROUND(SUM(je.cost_of_investment + je.unrealized_gl), 2), + ROUND(SUM(je.cash), 2), + ROUND(SUM(je.other_assets), 2), + ROUND(SUM(je.cost_of_investment + je.unrealized_gl + je.cash + je.other_assets), 2), + ROUND(SUM(je.loc_outstanding), 2), + ROUND(SUM(nav.unfunded_commitments), 2), + ROUND(SUM(je.cost_of_investment + je.unrealized_gl + je.cash + je.other_assets + nav.unfunded_commitments), 2), + ROUND(SUM(nav.ending_total_nav), 2), + ROUND(SUM(nav.ending_lp_nav), 2), + ROUND(SUM(nav.ending_gp_nav), 2), + ROUND(SUM(nav.ending_total_nav + nav.unfunded_commitments), 2), + ROUND(SUM(nav.cumulative_commitment_amount), 2), + ROUND(SUM(act.annual_subscriptions), 2), + ROUND(SUM(act.annual_distributions), 2), + ROUND(SUM(nav.cumulative_total_distributions), 2), + SUM(lp.count_lps), + SUM(lp.count_gps), + SUM(ps.active_portfolio_companies), + NULL, NULL, NULL +FROM funds f +LEFT JOIN je_balances je ON f.fund_uuid = je.fund_uuid +LEFT JOIN nav_data nav ON f.fund_uuid = nav.fund_uuid +LEFT JOIN annual_activity act ON f.fund_uuid = act.fund_uuid +LEFT JOIN lp_counts lp ON f.fund_uuid = lp.fund_uuid +LEFT JOIN portfolio_summary ps ON f.fund_uuid = ps.fund_uuid + +ORDER BY form_adv_section DESC, fund_name +``` + +## How to Present + +Separate the results into fund detail rows (`form_adv_section = '7.B.(1) Fund Detail'`) and the firm rollup row (`form_adv_section = '5.D. Firm Rollup'`). + +### Section 1 — Schedule D §7.B.(1) Fund-Level Detail + +| Fund | Type | Fair Market Value | Cash | Other Assets | Total Gross Assets | Unfunded Commitments | Regulatory AUM | NAV | LOC Outstanding | +|---|---|---|---|---|---|---|---|---|---| +| Fund I | Fund | $50,000,000 | $5,000,000 | $1,000,000 | $56,000,000 | $20,000,000 | $76,000,000 | $48,000,000 | $0 | + +### Section 2 — Capital Activity & Investor Counts + +| Fund | LP Investors | GP Investors | Portfolio Cos | Annual Subscriptions | Annual Distributions | Total Distributions (Inception) | Total Committed | Unfunded | +|---|---|---|---|---|---|---|---|---| + +### Section 3 — Performance Metrics (only for funds with NAV data) + +| Fund | NAV | LP NAV | GP NAV | Net AUM (Form PF) | DPI | TVPI | MOIC | +|---|---|---|---|---|---|---|---| + +### Section 4 — Item 5.D. Firm Rollup + +| | Amount | +|---|---| +| Fair Market Value | $X | +| Cash | $X | +| Other Assets | $X | +| Total Gross Assets | $X | +| Unfunded Commitments | $X | +| **Total Regulatory AUM (Form ADV)** | **$X** | +| Net Asset Value | $X | +| Net AUM (Form PF) | $X | + +### Formatting + +- Format currency as `$X,XXX,XXX` with no decimals +- Format multiples as `X.XXx` (e.g. `1.85x`) +- Use `—` for null values +- End with: *Data as of {reporting_date} · Balance sheet uses effective_date (accounting date) · NAV from monthly calculations* + +## Voice Guidelines + +- Say "your regulatory AUM" or "your Form ADV data" — not "query results" or "database data" +- Frame errors as "I wasn't able to retrieve that data" — not technical details +- FMV and NAV are different: FMV = investment portfolio only; NAV includes cash and other assets minus liabilities + +## Best Effort + +- **Computed:** firm rollup totals, presentation formatting +- **Authoritative:** all per-fund data values come directly from the Carta data warehouse diff --git a/plugins/carta-fund-admin/skills/performance-benchmarks/SKILL.md b/plugins/carta-fund-admin/skills/performance-benchmarks/SKILL.md new file mode 100644 index 0000000..4430101 --- /dev/null +++ b/plugins/carta-fund-admin/skills/performance-benchmarks/SKILL.md @@ -0,0 +1,148 @@ +--- +name: performance-benchmarks +description: Compare a fund's performance against peer benchmark cohorts. Use when asked about fund benchmarks, peer comparison, percentile ranking, Net IRR vs peers, TVPI benchmarks, or how a fund stacks up against its cohort. +--- + +# Performance Benchmarks + +Compare a fund's historical Net IRR, TVPI, MOIC, or DPI against peer benchmark cohorts grouped by vintage year, AUM bucket, and entity type. + +## When to Use + +- "How does Fund I compare to its benchmark?" +- "Show me Net IRR benchmarks for [Fund]" +- "What percentile is [Fund] in for TVPI?" +- "Compare [Fund] against vintage 2020 peers" +- "Show me benchmark data for our funds" +- "How does our fund stack up against peers?" + +## Prerequisites + +- The user must have the Carta MCP server connected +- A `fund_name` is required — **ask if not provided** +- If this is the first query, call `list_contexts` and `set_context` to set the firm + +### Optional Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `metric` | `net_irr` | One of: `net_irr`, `tvpi`, `moic`, `dpi` | +| `vintage_year` | Auto-resolved from fund | Filter cohort by vintage year | +| `aum_bucket` | Auto-resolved from fund | Filter by AUM bucket (e.g. `"25m-100m"`) | +| `entity_type` | Auto-resolved from fund | `"Fund"` or `"SPV"` | +| `start_date` | All history | Only show quarters on/after this date (YYYY-MM-DD) | + +## Data Retrieval + +Query the `FUND_ADMIN.TEMPORAL_FUND_COHORT_BENCHMARKS` table which contains fund metrics, benchmark percentiles, and cohort size in one denormalized table. + +### SQL Query + +Execute this query with `execute_query`, substituting the user's fund name and optional filters: + +```sql +SELECT + fund_name, + fund_uuid, + performance_quarter_start_date AS quarter, + vintage_year, + fund_aum_bucket, + entity_type_name, + fund_count, + net_irr, tvpi, dpi, moic, + net_irr_10th, net_irr_25th, net_irr_50th, net_irr_75th, net_irr_90th, + tvpi_10, tvpi_25, tvpi_50, tvpi_75, tvpi_90, + dpi_10, dpi_25, dpi_50, dpi_75, dpi_90, + moic_10, moic_25, moic_50, moic_75, moic_90 +FROM FUND_ADMIN.TEMPORAL_FUND_COHORT_BENCHMARKS +WHERE fund_name ILIKE '%{fund_name}%' + AND performance_quarter_start_date >= '{start_date}' + -- Add optional filters: + -- AND vintage_year = {vintage_year} + -- AND fund_aum_bucket = '{aum_bucket}' + -- AND entity_type_name = '{entity_type}' +ORDER BY performance_quarter_start_date +LIMIT 1000 +``` + +**Important:** Build the WHERE clause dynamically — only include filters the user provided. Omitted filters are auto-resolved from the data. + +### Handling Multiple Fund Matches + +If the query returns rows for more than one distinct `fund_name`, list them and ask the user to be more specific: + +> Multiple funds matched "[search term]": +> - Fund I +> - Fund II +> +> Please use a more specific fund name. + +## Metric Configuration + +| Metric | Column | Format | Percentile Columns | +|--------|--------|--------|--------------------| +| Net IRR | `net_irr` | Percentage (e.g. `8.0%`) | `net_irr_10th` through `net_irr_90th` | +| TVPI | `tvpi` | Multiple (e.g. `1.85x`) | `tvpi_10` through `tvpi_90` | +| MOIC | `moic` | Multiple (e.g. `2.10x`) | `moic_10` through `moic_90` | +| DPI | `dpi` | Multiple (e.g. `0.42x`) | `dpi_10` through `dpi_90` | + +### Percentile Band Logic + +Based on the fund's value relative to benchmarks in the latest quarter: + +| Condition | Label | +|-----------|-------| +| Fund ≥ 90th percentile | Top 10th percentile | +| Fund ≥ 75th percentile | 75th–90th percentile | +| Fund ≥ 50th percentile | 50th–75th percentile (above median) | +| Fund ≥ 25th percentile | 25th–50th percentile (below median) | +| Fund < 25th percentile | Bottom 25th percentile | + +## How to Present + +Use the **latest quarter** (last row) for the summary, all rows for the timeline. + +### Section 1 — Cohort Details + +| | | +|---|---| +| Fund | {fund_name} | +| Metric | {metric_label} | +| Vintage Year | {vintage_year} | +| AUM Bucket | {aum_bucket} | +| Entity Type | {entity_type} | +| Date Range | {first_quarter} – {last_quarter} | +| Benchmark Sample Size | **{fund_count} funds** | + +### Section 2 — Current Standing + +| | {metric_label} | +|---|---| +| **{fund_name}** | **{fund_value}** | +| Benchmark Median (50th pct) | {p50} | +| Benchmark 75th pct | {p75} | +| Benchmark 90th pct | {p90} | +| Fund Percentile Band | {band_label} | + +### Section 3 — Timeline by Quarter + +| Quarter | {fund_name} | 10th pct | 25th pct | Median | 75th pct | 90th pct | Cohort (n) | +|---|---|---|---|---|---|---|---| +| 2023-01-01 | 8.5% | 2.1% | 5.0% | 7.8% | 10.2% | 14.5% | 45 | + +### Formatting + +- Net IRR: display as `X.X%` (values are stored as percentage points, e.g. 8.0 = 8%) +- TVPI/MOIC/DPI: display as `X.XXx` +- Use `—` for null values +- End with: *Cohort: {vintage} vintage · {bucket} AUM · {entity_type} · {count} funds as of latest quarter* + +## Voice Guidelines + +- Say "your fund ranks in the top quartile" not "the query returned a value above the 75th percentile" +- Frame the percentile band in plain language: "Fund I is performing above the median for its peer group" + +## Best Effort + +- **Computed:** percentile band classification, trend analysis +- **Authoritative:** fund metrics and benchmark percentiles come directly from the Carta data warehouse