THE ENTITY Data · Developer API
The data behind THE ENTITY, as an API
Read-only access to the four datasets that power the platform — the causal supply-chain graph, the computed engine, the tracked-elite smart-money panel, and market feeds& cited filings — over REST, MCP, Sheets and bulk export. Authenticate with an entk_live_ Bearer key; every call is metered and tiered. 29 endpoints, one stable envelope, no PII, no writes.
Building an AI agent? This is the MCP server for AI supply-chain data — the graph, engine and smart-money as native tools for Claude, Cursor or your own agent.
Overview
THE ENTITY Data sells programmatic, read-only access to four datasets. Every dataset is exposed over the same conventions, and the same key can reach any dataset its scopes allow.
graph— The causal value-chain graph — nodes, typed relations, company edges, facilities, chokepoints and the layer taxonomy.engine— Computed exhibits and the daily record — nowcast, min-cut, seeded Monte-Carlo forecast, conviction and node-score history.smart-money— The tracked-elite 13F panel — roster, consensus, the archive tape, clone book, fund books and company holder rosters. Counts are of the TRACKED panel, not institutions at large.feeds— Market microstructure and grounded citations — insider trades/positions, ownership events, short interest/volume, and verbatim cited chain mentions + corroborated extracted metrics.
Everything is read-only (only GET), carries no personal data, and returns the stable envelope described below. The base URL is https://joinentity.com/api/v1.
Recipes
Four things people build with THE ENTITY Data. Each is a few calls with the same entk_live_ key — copy, swap in your key, and go.
Supply-chain risk dashboard
Where is the AI chain tightest, and who’s exposed? Join the structural pinch points, the live tightness index, and where the tracked elite are crowding.
Endpoints: /graph/chokepoints · /engine/nowcast · /smart-money/consensus
# the structural pinch points, ranked
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/chokepoints?sort=exposureScore&order=desc&limit=10"
# system tightness right now (0-100, per layer)
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/engine/nowcast"
# names ≥3 tracked-elite funds hold, joined to the verdict
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/consensus?min_holders=3"Feed an AI agent (MCP)
Point Claude, Cursor or your own agent at the MCP server — it gets the graph, engine and smart-money reads as native tools, authenticated by your key.
Endpoint: /mcp (Streamable HTTP, JSON-RPC 2.0)
// e.g. an MCP client config
{
"mcpServers": {
"the-entity": {
"url": "https://joinentity.com/api/v1/mcp",
"headers": { "Authorization": "Bearer $ENTITY_KEY" }
}
}
}13F consensus in your model
Pull the tracked-elite consensus and any fund’s archived book straight into a notebook or sheet. Counts are of the tracked panel — labeled, never inflated.
Endpoints: /smart-money/consensus · /smart-money/funds/{cik}/holdings
import requests
H = {"Authorization": f"Bearer {KEY}"}
consensus = requests.get("https://joinentity.com/api/v1/smart-money/consensus",
headers=H, params={"min_holders": 3}).json()["data"]
# one fund's archived 13F book across quarters
book = requests.get("https://joinentity.com/api/v1/smart-money/funds/1067983/holdings",
headers=H).json()["data"]Screen for accumulation the engine backs
One call: companies the tracked elite are net-accumulating andthe engine has conviction on — ranked by net adds. The platform’s core thesis as a screener.
Endpoint: /screener/accumulation (or compose /smart-money/tape + /engine/node-scores yourself)
# net-accumulated + engine conviction ≥ 60, most-added first
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/screener/accumulation?min_holders=3&min_conviction=60"Prefer no code? The same data lands in Sheets & Excel and as bulk CSV / NDJSON.
For AI agents
THE ENTITY Data is built to be an agent’s tool for the AI supply chain. The MCP serverexposes the graph, engine and smart-money reads as native tools — your assistant calls them directly, authenticated by the user’s entk_live_ key, read-only (no writes, no trades). Point Claude, Cursor or your own agent at it:
{
"mcpServers": {
"the-entity": {
"url": "https://joinentity.com/api/v1/mcp",
"headers": { "Authorization": "Bearer $ENTITY_KEY" }
}
}
}- Transport: Streamable HTTP, JSON-RPC 2.0 at
https://joinentity.com/api/v1/mcp. - Tools = the read + personal surface of THE ENTITY (search the graph, pull smart-money, engine exhibits, feeds). Never propose/admin, never a trade.
- Every result carries the same honesty labels as the REST envelope (
as_ofvsdetected_at, tracked-panel counts).
Answer engines (ChatGPT, Perplexity, Claude, Google AI Overviews) can also discover the platform via /llms.txt.
Authentication
Every data request needs a key, sent as a bearer token (the x-api-key header is also accepted). Keys are minted in your account settings after subscribing — see pricing. Keys are prefixed entk_live_.
Test a key end-to-end against /whoami — it runs the full authorizer and returns your tier, scopes and live usage (and consumes exactly one metered call):
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/whoami"Tiers
Every tier reaches the same datasets; they differ only by monthly call quota, per-minute rate, key cap and the surfaces they may use. bulk is a Pro/Scale surface.
| Tier | Price | Monthly calls | Rate / min | Max keys | Surfaces |
|---|---|---|---|---|---|
| ENTITY Data · Starter | $79/mo | 10,000 | 60 | 2 | rest, mcp |
| ENTITY Data · Pro | $149/mo | 100,000 | 300 | 10 | rest, mcp, bulk |
| ENTITY Data · Scale | $499/mo | 1,000,000 | 600 | 25 | rest, mcp, bulk |
Quota is an account pool across all your keys. The values above are the live tier table — never hardcoded here.
Rate & quota headers
Every authorized reply — success, degraded read, or a 429 — carries the rate and quota state, so you never have to guess how much headroom is left.
| Header | Meaning |
|---|---|
| x-ratelimit-limit | Per-minute request ceiling for the account. |
| x-ratelimit-remaining | Requests left in the current minute window. |
| x-ratelimit-reset | Unix time (seconds) when the rate window resets. |
| x-quota-limit | Monthly call allowance for the tier. |
| x-quota-remaining | Calls left in the monthly allowance. |
| x-quota-reset | ISO timestamp when the monthly quota resets. |
| retry-after | Seconds to wait before retrying (present on 429). |
Both throttles surface as 429 but mean different things: rate_limited is the per-minute ceiling (retry after retry-after seconds); quota_exceeded is the monthly allowance (resets at x-quota-reset).
Response envelope
Every data endpoint returns the same shape. Clients branch on the top-level ok.
{
"ok": true,
"data": ... , // object | array | null (null only when degraded)
"page": { // present on paginated collections
"limit": 50, "offset": 0,
"count": 50, "total": 812,
"hasMore": true, "nextOffset": 50
},
"as_of": "2026-06-30", // the data's own effective/observation date (ISO) or null
"detected_at": "2026-07-02", // when WE ingested it (ISO) or null — NEVER conflated with as_of
"meta": { ... } // honesty labels: tracked_funds, display_only/source, degraded, …
}On a gate failure the body is instead { "ok": false, "error": "…" } with the matching status. Pagination is capped server-side: ask for limit=99999 and you get 200 rows, not an error.
Endpoints
29 endpoints across the four datasets. The full machine spec, with every parameter and schema, is at /api/v1/openapi.json.
?fields=a,b,c— keep only those top-level fields in each object (smaller payloads).?sort=field+?order=asc|desc— order a listed collection (nulls last), applied before paging.GET /search?q=…— ranked full-text search across nodes, domains, themes and companies; follow each hit’sidinto its typed endpoint.
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/nodes?layerId=chips&sort=title&fields=id,title,layerId&limit=5"Graph graph
The causal value-chain graph — nodes, typed relations, company edges, facilities, chokepoints and the layer taxonomy.
Full-text search across the graph — nodes, domains, themes and companies (ranked).
Params: q (required), limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/search?q=hbm"The five value-chain layers and their domains (full taxonomy).
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/layers"List causal-graph nodes (summary projection), filterable + paginated.
Params: layerId, domainId, status, complexity, timeHorizon, q, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/nodes?layerId=chips&limit=20"One node, full detail (all summaries, watch items, metrics, sources).
Params: id
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/nodes/{id}"Company↔company relationships (supplier/customer/partner/competitor).
Params: fromCompanyId, toCompanyId, company, type, nodeId, confidenceTier, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/edges?company=nvidia"Typed node→node causal relations (strength, elasticity, latency).
Params: sourceNodeId, targetNodeId, nodeId, type, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/relations?nodeId=foundry-capacity"Physical sites (fabs, plants, ports) geolocated + hooked to nodes.
Params: companyId, geographyId, nodeId, kind, status, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/facilities?geographyId=taiwan"Structural pinch points (straits, ports, jurisdictions) + affected nodes.
Params: kind, nodeId, geographyId, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/chokepoints?kind=strait"One company's exposures to chain nodes (intensity 0–100).
Params: id, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/graph/companies/{id}/exposures"Grounded, verbatim value-chain mentions from earnings calls (cited, source-linked).
Params: company (required), node, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/cited/chain-mentions?company=NVDA&node=hbm"Corroboration-gated metric series extracted from cited sources (uncorroborated suppressed).
Params: node, metric, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/cited/extracted-metrics?node=cowos"Engine engine
Computed exhibits and the daily record — nowcast, min-cut, seeded Monte-Carlo forecast, conviction and node-score history.
Constraint Nowcast — system tightness index (0–100), per-layer + movers.
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/engine/nowcast"Min cut of the Energy→Applications flow network (structural exhibit).
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/engine/min-cut"Probabilistic bottleneck-migration forecast (seeded Monte-Carlo).
Params: horizonQuarters, runs
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/engine/forecast?horizonQuarters=6"Conviction record from D1 (per-company score/verdict/drivers).
Params: company, date, limit
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/engine/conviction?company=nvidia"Daily node-score record from D1 (global/tension/news/timing).
Params: node, days, from, to
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/engine/node-scores?node=hbm&days=30"Smart Money smart-money
The tracked-elite 13F panel — roster, consensus, the archive tape, clone book, fund books and company holder rosters. Counts are of the TRACKED panel, not institutions at large.
DERIVED screen: companies the tracked elite are net-accumulating AND the engine agrees (ranked).
Params: min_holders, min_adding, min_conviction, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/screener/accumulation?min_holders=3&min_conviction=60"The tracked-elite fund roster (optionally scored), + a labeled platform sidecar.
Params: scores, include_platform, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/funds?scores=true"One fund's archived 13F book across quarters (filing-dated archive).
Params: cik, from_quarter, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/funds/{cik}/holdings"Universe consensus: companies ≥1 tracked-elite fund holds, joined to the verdict.
Params: min_holders, convergence, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/consensus?min_holders=3"THE TAPE — archive-basis fund entry/exit/accel/cool events (display-only).
Params: before, material_pct, type, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/tape?type=entry"The Long/Short Clone Book snapshot + backtest curve (display-only, filing-archive).
Params: curve
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/clone-book"One company's FULL-census 13F holder history (all filers, the honest denominator).
Params: company, quarters
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/companies/{company}/census"The tracked-elite fund book for one company (value ladder + optional trajectory).
Params: company, history, include_platform
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/smart-money/companies/{company}/holders?history=90"Feeds & Cited feeds
Market microstructure and grounded citations — insider trades/positions, ownership events, short interest/volume, and verbatim cited chain mentions + corroborated extracted metrics.
SEC Form 4 open-market insider buys/sells (transaction-dated, not our detection).
Params: company, person_cik, since_days, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/feeds/insider-trades?company=NVDA"SEC Form 3/5 insider holdings (positions owned) — periodOfReport / filedAt / detectedAt.
Params: company (required), limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/feeds/insider-positions?company=NVDA"Filing-sourced ownership events (13D/G, Form 144, congress PTRs, new-insiders, reveals).
Params: company, window_days, kinds, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/feeds/ownership-events?window_days=60"FINRA bi-monthly short interest — a symbol's cycles, or the latest crowded names.
Params: ticker, company, cycles, mode, min_dtc, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/feeds/short-interest?ticker=NVDA"FINRA daily short-volume (tradeDate vs detectedAt; regSho tri-state, never coerced).
Params: ticker, company, days, limit, offset
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/feeds/short-volume?ticker=NVDA&days=30"Sheets & Excel
Sheets and Excel are just REST clients of the API above — the same entk_live_ key drives them, on any tier with a REST scope. Nothing extra to buy. Paste the code below; no Marketplace or Office Store install needed. Your key is stored privately and sent only in the request header — never put it in a cell.
Google Sheets — the =ENTITY() function
- In your sheet:
Extensions ▸ Apps Script. Replace the sample with theCode.gsbelow. - Click
+ ▸ HTML, name the file exactlySidebar, and paste the Sidebar snippet. - Save, then reload the sheet — a
THE ENTITY Datamenu appears. THE ENTITY Data ▸ Set API keyonce (this also grants the one-time network permission).
Then pull data live into any cell:
=ENTITY("/smart-money/funds","scores=true")
=ENTITY("/graph/nodes","q=hbm&limit=20")
=ENTITY("/engine/forecast","horizonQuarters=6")
=ENTITY("/graph/nodes/hbm") ' one node, full detailThe result spills into empty cells to the right and down. For large or fully-paginated pulls, use THE ENTITY Data ▸ Pull to table— it writes a static table (no 30-second cell limit) and is the right path on sheets shared with other editors, since it runs under each editor’s own key. In a cell, =ENTITY() reads the sheet owner’s key (a Google restriction), so on a shared sheet prefer the sidebar. =ENTITY() refreshes when its arguments change, not on a timer — pass a cell you bump as the third argument to force a refresh.
Code.gs
/**
* THE ENTITY Data — Google Sheets client.
*
* INSTALL (no Marketplace needed):
* 1. Extensions > Apps Script. Delete any sample, paste THIS file as "Code.gs".
* 2. Add an HTML file named exactly "Sidebar" and paste the Sidebar snippet.
* 3. Save, then reload the Sheet. A "THE ENTITY Data" menu appears.
* 4. THE ENTITY Data > Set API key — paste your entk_live_ key ONCE
* (this also grants the one-time network permission; custom functions
* can't prompt for it themselves).
* Then: =ENTITY("/graph/nodes","q=hbm") — or THE ENTITY Data > Pull to table.
*
* Your key is stored in your user properties (never in a cell) and sent only in
* the Authorization header. Read-only: every call is a GET.
*
* SHARED SHEETS: by a Google rule, =ENTITY() in a cell reads the SHEET OWNER'S
* key (not the invoking editor's). So on a solo/owned sheet =ENTITY is fully
* per-you; on a sheet shared with other editors, those editors should use
* "THE ENTITY Data > Pull to table" (the sidebar runs as the active user and
* reads THAT user's key), not =ENTITY.
*/
var ENTITY_BASE = 'https://joinentity.com/api/v1';
var ENTITY_KEY_PROP = 'ENTITY_API_KEY';
var ENTITY_PAGE_LIMIT = 200; // server max page size
var ENTITY_CELL_ROW_CAP = 1000; // =ENTITY() in-cell row cap (the 30s wall)
var ENTITY_CELL_TIME_MS = 25000; // =ENTITY() time budget, under the 30s wall
var ENTITY_IMPORT_ROW_CAP = 50000; // sidebar "Pull to table" cap
var ENTITY_IMPORT_TIME_MS = 300000; // sidebar time budget (server ~6-min limit)
/* ---- menu + key management (public: reachable from the menu / dialog) ---- */
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('THE ENTITY Data')
.addItem('Set API key…', 'entitySetKey')
.addItem('Test connection', 'entityTestConnection')
.addSeparator()
.addItem('Pull to table…', 'entityShowSidebar')
.addSeparator()
.addItem('Clear API key', 'entityClearKey')
.addToUi();
}
function entitySetKey() {
var html = HtmlService.createHtmlOutput(
'<div style="font:13px -apple-system,Segoe UI,Roboto,sans-serif;padding:4px">' +
'<p>Paste your <b>entk_live_</b> key. Stored privately (never in a cell).</p>' +
'<form onsubmit="return save()">' +
'<input id="k" type="password" placeholder="entk_live_…" style="width:100%;box-sizing:border-box;padding:6px">' +
'<p style="text-align:right;margin:10px 0 0"><button type="submit">Save key</button></p></form>' +
'<script>function save(){var v=document.getElementById("k").value;' +
'google.script.run.withSuccessHandler(function(){google.script.host.close()})' +
'.withFailureHandler(function(e){alert(e.message)}).entitySaveKey(v);return false;}</script></div>'
).setWidth(380).setHeight(150);
SpreadsheetApp.getUi().showModalDialog(html, 'Set API key');
}
// PUBLIC (no trailing underscore) so the dialog's google.script.run can reach it.
function entitySaveKey(key) {
key = String(key || '').trim();
if (!key) throw new Error('No key provided.');
PropertiesService.getUserProperties().setProperty(ENTITY_KEY_PROP, key);
// Touch the network so the script.external_request scope is granted now.
try { UrlFetchApp.fetch(ENTITY_BASE + '/whoami', { method: 'get', muteHttpExceptions: true, headers: { Authorization: 'Bearer ' + key } }); } catch (e) {}
return true;
}
function entityClearKey() {
PropertiesService.getUserProperties().deleteProperty(ENTITY_KEY_PROP);
SpreadsheetApp.getUi().alert('THE ENTITY Data', 'API key cleared.', SpreadsheetApp.getUi().ButtonSet.OK);
}
function entityTestConnection() {
var ui = SpreadsheetApp.getUi();
try {
// /whoami is NOT enveloped — tier/scopes/usage sit at the top level.
var body = entityFetch_('/whoami', '');
var info = { tier: body.tier, scopes: body.scopes, usage: body.usage };
ui.alert('THE ENTITY Data', 'Connected. ' + JSON.stringify(info).slice(0, 400), ui.ButtonSet.OK);
} catch (e) {
ui.alert('THE ENTITY Data', 'Failed: ' + e.message, ui.ButtonSet.OK);
}
}
function entityShowSidebar() {
var html = HtmlService.createHtmlOutputFromFile('Sidebar').setTitle('THE ENTITY Data — Pull to table');
SpreadsheetApp.getUi().showSidebar(html);
}
/* ---- the =ENTITY() custom function ---- */
/**
* Pull a THE ENTITY Data endpoint into cells (spills to the right + down).
* =ENTITY("/smart-money/funds","scores=true")
* =ENTITY("/graph/nodes/hbm")
* @param {string} path Endpoint path, e.g. "/graph/nodes".
* @param {string} query Optional query string, e.g. "q=hbm&limit=20".
* @param {any} refreshToken Optional: reference a cell you bump to force a refresh
* (custom functions do not auto-refresh on a timer).
* @return A table (header row + rows), or a readable message.
* @customfunction
*/
function ENTITY(path, query, refreshToken) {
if (path === undefined || path === null || path === '') return 'Usage: =ENTITY("/graph/nodes","q=hbm")';
try {
var res = entityCollect_(String(path), query == null ? '' : String(query), ENTITY_CELL_ROW_CAP, ENTITY_CELL_TIME_MS);
if (res.degraded) return 'THE ENTITY: ' + res.degraded;
if (!res.rows.length) return [['(no rows)' + (res.note ? ' — ' + res.note : '')]];
return res.rows;
} catch (e) {
return 'THE ENTITY: ' + e.message;
}
}
/* ---- sidebar server function (PUBLIC: reached via google.script.run) ---- */
function entityImportToActiveCell(path, query) {
var res = entityCollect_(String(path || ''), String(query || ''), ENTITY_IMPORT_ROW_CAP, ENTITY_IMPORT_TIME_MS);
if (res.degraded) throw new Error(res.degraded);
var rows = res.rows.length ? res.rows : [['(no rows)' + (res.note ? ' — ' + res.note : '')]];
var width = 1;
for (var i = 0; i < rows.length; i++) width = Math.max(width, rows[i].length);
for (var j = 0; j < rows.length; j++) { while (rows[j].length < width) rows[j].push(''); }
var sheet = SpreadsheetApp.getActiveSheet();
var cell = sheet.getActiveCell();
var range = sheet.getRange(cell.getRow(), cell.getColumn(), rows.length, width);
range.setValues(rows);
// Honesty note on the top-left cell: the data's own date vs when we ingested it.
var note = [];
if (res.asOf) note.push('as_of: ' + res.asOf);
if (res.detectedAt) note.push('detected_at: ' + res.detectedAt);
if (res.capped) note.push('(capped at ' + ENTITY_IMPORT_ROW_CAP + ' rows)');
else if (res.note) note.push('(' + res.note + ')');
if (note.length) sheet.getRange(cell.getRow(), cell.getColumn()).setNote(note.join('\n'));
return { rows: rows.length, cols: width, capped: !!res.capped };
}
// The endpoint catalog for the sidebar dropdown (PUBLIC: read via google.script.run).
function entityEndpointCatalog() {
return [{"path":"/search","dataset":"graph","summary":"Full-text search across the graph — nodes, domains, themes and companies (ranked).","example":"/search?q=hbm"},{"path":"/graph/layers","dataset":"graph","summary":"The five value-chain layers and their domains (full taxonomy).","example":"/graph/layers"},{"path":"/graph/nodes","dataset":"graph","summary":"List causal-graph nodes (summary projection), filterable + paginated.","example":"/graph/nodes?layerId=chips&limit=20"},{"path":"/graph/nodes/{id}","dataset":"graph","summary":"One node, full detail (all summaries, watch items, metrics, sources).","example":"/graph/nodes/{id}"},{"path":"/graph/edges","dataset":"graph","summary":"Company↔company relationships (supplier/customer/partner/competitor).","example":"/graph/edges?company=nvidia"},{"path":"/graph/relations","dataset":"graph","summary":"Typed node→node causal relations (strength, elasticity, latency).","example":"/graph/relations?nodeId=foundry-capacity"},{"path":"/graph/facilities","dataset":"graph","summary":"Physical sites (fabs, plants, ports) geolocated + hooked to nodes.","example":"/graph/facilities?geographyId=taiwan"},{"path":"/graph/chokepoints","dataset":"graph","summary":"Structural pinch points (straits, ports, jurisdictions) + affected nodes.","example":"/graph/chokepoints?kind=strait"},{"path":"/graph/companies/{id}/exposures","dataset":"graph","summary":"One company's exposures to chain nodes (intensity 0–100).","example":"/graph/companies/{id}/exposures"},{"path":"/engine/nowcast","dataset":"engine","summary":"Constraint Nowcast — system tightness index (0–100), per-layer + movers.","example":"/engine/nowcast"},{"path":"/engine/min-cut","dataset":"engine","summary":"Min cut of the Energy→Applications flow network (structural exhibit).","example":"/engine/min-cut"},{"path":"/engine/forecast","dataset":"engine","summary":"Probabilistic bottleneck-migration forecast (seeded Monte-Carlo).","example":"/engine/forecast?horizonQuarters=6"},{"path":"/engine/conviction","dataset":"engine","summary":"Conviction record from D1 (per-company score/verdict/drivers).","example":"/engine/conviction?company=nvidia"},{"path":"/engine/node-scores","dataset":"engine","summary":"Daily node-score record from D1 (global/tension/news/timing).","example":"/engine/node-scores?node=hbm&days=30"},{"path":"/screener/accumulation","dataset":"smart-money","summary":"DERIVED screen: companies the tracked elite are net-accumulating AND the engine agrees (ranked).","example":"/screener/accumulation?min_holders=3&min_conviction=60"},{"path":"/smart-money/funds","dataset":"smart-money","summary":"The tracked-elite fund roster (optionally scored), + a labeled platform sidecar.","example":"/smart-money/funds?scores=true"},{"path":"/smart-money/funds/{cik}/holdings","dataset":"smart-money","summary":"One fund's archived 13F book across quarters (filing-dated archive).","example":"/smart-money/funds/{cik}/holdings"},{"path":"/smart-money/consensus","dataset":"smart-money","summary":"Universe consensus: companies ≥1 tracked-elite fund holds, joined to the verdict.","example":"/smart-money/consensus?min_holders=3"},{"path":"/smart-money/tape","dataset":"smart-money","summary":"THE TAPE — archive-basis fund entry/exit/accel/cool events (display-only).","example":"/smart-money/tape?type=entry"},{"path":"/smart-money/clone-book","dataset":"smart-money","summary":"The Long/Short Clone Book snapshot + backtest curve (display-only, filing-archive).","example":"/smart-money/clone-book"},{"path":"/smart-money/companies/{company}/census","dataset":"smart-money","summary":"One company's FULL-census 13F holder history (all filers, the honest denominator).","example":"/smart-money/companies/{company}/census"},{"path":"/smart-money/companies/{company}/holders","dataset":"smart-money","summary":"The tracked-elite fund book for one company (value ladder + optional trajectory).","example":"/smart-money/companies/{company}/holders?history=90"},{"path":"/feeds/insider-trades","dataset":"feeds","summary":"SEC Form 4 open-market insider buys/sells (transaction-dated, not our detection).","example":"/feeds/insider-trades?company=NVDA"},{"path":"/feeds/insider-positions","dataset":"feeds","summary":"SEC Form 3/5 insider holdings (positions owned) — periodOfReport / filedAt / detectedAt.","example":"/feeds/insider-positions?company=NVDA"},{"path":"/feeds/ownership-events","dataset":"feeds","summary":"Filing-sourced ownership events (13D/G, Form 144, congress PTRs, new-insiders, reveals).","example":"/feeds/ownership-events?window_days=60"},{"path":"/feeds/short-interest","dataset":"feeds","summary":"FINRA bi-monthly short interest — a symbol's cycles, or the latest crowded names.","example":"/feeds/short-interest?ticker=NVDA"},{"path":"/feeds/short-volume","dataset":"feeds","summary":"FINRA daily short-volume (tradeDate vs detectedAt; regSho tri-state, never coerced).","example":"/feeds/short-volume?ticker=NVDA&days=30"},{"path":"/cited/chain-mentions","dataset":"graph","summary":"Grounded, verbatim value-chain mentions from earnings calls (cited, source-linked).","example":"/cited/chain-mentions?company=NVDA&node=hbm"},{"path":"/cited/extracted-metrics","dataset":"graph","summary":"Corroboration-gated metric series extracted from cited sources (uncorroborated suppressed).","example":"/cited/extracted-metrics?node=cowos"}];
}
/* ---- internals (trailing underscore = private to the script) ---- */
// Fetch ONE page. Throws a readable Error on any {ok:false}/HTTP>=400.
function entityFetch_(path, query) {
var url = ENTITY_BASE + entityNormPath_(path);
if (query) url += (url.indexOf('?') >= 0 ? '&' : '?') + String(query).replace(/^[?&]/, '');
var resp = UrlFetchApp.fetch(url, {
method: 'get',
muteHttpExceptions: true,
headers: { Authorization: 'Bearer ' + entityGetKey_() },
});
var code = resp.getResponseCode();
var body = {};
try { body = JSON.parse(resp.getContentText() || '{}'); } catch (e) { body = {}; }
if (code >= 400 || body.ok === false) {
throw new Error(entityErrMsg_((body && body.error) || ('http_' + code), resp));
}
return body;
}
// Collect (paginating under caps) + flatten to a 2-D array. Buffers all rows so
// the header set is the UNION across every page (a key that first appears on
// page 2 still becomes a column). Returns { rows, asOf, detectedAt, meta, capped,
// degraded?, note? }.
function entityCollect_(path, query, rowCap, timeMs) {
var start = Date.now();
var objects = []; // accumulated row-objects (array-of-objects endpoints)
var primitives = []; // accumulated primitive rows (array-of-primitives)
var kv = null; // a single detail object → field/value
var offset = 0;
var userPaged = /(?:^|[?&])(?:limit|offset)=/.test(query || '');
var last = null;
var capped = false;
var sawCollection = false;
var stoppedEarly = false;
while (true) {
var q = userPaged ? query : entityMergeQuery_(query, 'limit=' + ENTITY_PAGE_LIMIT + '&offset=' + offset);
var body = entityFetch_(path, q);
last = body;
var data = body.data;
if (data === null || data === undefined) {
if (sawCollection) { stoppedEarly = true; break; } // a later page degraded — flag it, don't present partial as full
return { rows: [], degraded: 'unavailable' + (entityMetaNote_(body) ? ' — ' + entityMetaNote_(body) : ''), meta: body.meta || null };
}
// The collection: an array, or (defensive) the sole array field of an object
// that carries a top-level page — a nested paginated collection.
var coll = null;
if (Array.isArray(data)) coll = data;
else if (data && typeof data === 'object' && body.page) coll = entitySoleArrayField_(data);
if (coll !== null) {
sawCollection = true;
for (var i = 0; i < coll.length; i++) {
var v = coll[i];
if (v !== null && typeof v === 'object') objects.push(v); else primitives.push(v);
if (objects.length + primitives.length >= rowCap) { capped = true; break; }
}
var page = body.page;
if (userPaged || capped || !page || page.hasMore !== true) break;
offset = (page.nextOffset !== null && page.nextOffset !== undefined) ? page.nextOffset : (offset + ENTITY_PAGE_LIMIT);
if (Date.now() - start > timeMs) { capped = true; break; }
continue;
}
if (data && typeof data === 'object') { kv = data; break; } // detail object (not paginated)
primitives.push(data); break; // a bare primitive
}
var out = [];
if (kv) {
out.push(['field', 'value']);
var keys = Object.keys(kv);
for (var k = 0; k < keys.length; k++) out.push([keys[k], entityCell_(kv[keys[k]])]);
} else if (objects.length) {
var headers = entityUnionKeys_(objects); // union across ALL pages
out.push(headers);
for (var r = 0; r < objects.length; r++) {
var o = objects[r];
out.push(headers.map(function (h) { return entityCell_(o ? o[h] : null); }));
}
} else if (primitives.length) {
out.push(['value']);
for (var p = 0; p < primitives.length; p++) out.push([entityCell_(primitives[p])]);
} else {
// Empty collection — valid, just no rows. Surface any meta status honestly.
return { rows: [], note: entityMetaNote_(last) || 'no rows', asOf: last ? (last.as_of || null) : null, meta: last ? (last.meta || null) : null };
}
return {
rows: out,
asOf: last ? (last.as_of || null) : null,
detectedAt: last ? (last.detected_at || null) : null,
meta: last ? (last.meta || null) : null,
capped: capped,
note: stoppedEarly ? 'incomplete — a later page was unavailable' : (capped ? 'capped' : ''),
};
}
function entityGetKey_() {
var k = PropertiesService.getUserProperties().getProperty(ENTITY_KEY_PROP);
if (!k) throw new Error('set your API key first (THE ENTITY Data > Set API key).');
return k;
}
function entityNormPath_(path) {
path = String(path || '').trim();
if (path.indexOf('http') === 0) path = path.replace(ENTITY_BASE, ''); // tolerate a full URL
if (path.charAt(0) !== '/') path = '/' + path;
return path;
}
function entityMergeQuery_(base, extra) {
base = String(base || '').replace(/^[?&]/, '');
return base ? base + '&' + extra : extra;
}
function entityUnionKeys_(arr) {
var keys = [];
var seen = {};
for (var i = 0; i < arr.length; i++) {
var o = arr[i];
if (o && typeof o === 'object') {
for (var k in o) { if (Object.prototype.hasOwnProperty.call(o, k) && !seen[k]) { seen[k] = 1; keys.push(k); } }
}
}
return keys.length ? keys : ['value'];
}
function entityCell_(v) {
if (v === null || v === undefined) return '';
if (typeof v === 'object') { try { return JSON.stringify(v); } catch (e) { return String(v); } }
return v;
}
// A PURE single-list wrapper's array (e.g. { rows: [...] }), else null. A record
// that carries a list ALONGSIDE other fields (a detail object like census
// { latest, trend, … }) is NOT a wrapper and must keep all its fields.
function entitySoleArrayField_(obj) {
var keys = Object.keys(obj);
if (keys.length === 1 && Array.isArray(obj[keys[0]])) return obj[keys[0]];
return null;
}
// A short human status from the envelope meta (empty / rebuilding / not found …), or ''.
function entityMetaNote_(body) {
var m = body && body.meta;
if (!m) return '';
if (m.reason) return String(m.reason);
if (m.rebuilding) return 'rebuilding';
if (m.empty) return 'no data yet';
if (m.found === false) return 'not found';
if (m.available === false) return 'not available';
if (m.degraded) return 'degraded';
if (m.note) return String(m.note);
return '';
}
function entityErrMsg_(code, resp) {
var map = {
missing_key: 'no API key sent',
invalid_key: 'invalid API key',
inactive_addon: 'your THE ENTITY Data plan is not active',
surface_not_in_tier: 'this surface is not in your tier',
scope_denied: 'your key lacks the scope for this dataset',
rate_limited: 'rate limited',
quota_exceeded: 'monthly quota exhausted',
unavailable: 'temporarily unavailable — try again shortly',
};
var msg = map[code] || code;
if (code === 'rate_limited' && resp) {
try {
var h = resp.getHeaders() || {};
var ra = h['Retry-After'] || h['retry-after'];
if (ra) msg += ' — retry in ' + ra + 's';
} catch (e) {}
}
return msg;
}
Sidebar (HTML file)
<!-- THE ENTITY Data — add this as an HTML file named exactly "Sidebar". -->
<!DOCTYPE html>
<html>
<head><base target="_top">
<style>
body { font: 13px -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; padding: 12px; color: #1a1c1f; }
label { display: block; margin: 10px 0 4px; font-weight: 600; }
select, input { width: 100%; box-sizing: border-box; padding: 6px; }
button { margin-top: 12px; padding: 8px 12px; cursor: pointer; }
.hint { color: #6b7076; font-size: 12px; margin: 6px 0 0; }
#status { margin-top: 12px; white-space: pre-wrap; }
</style>
</head>
<body>
<p class="hint">Pulls the FULL result (all pages) into a static table starting at your active cell.</p>
<label for="ep">Endpoint</label>
<select id="ep"><option value="">Loading…</option></select>
<label for="path">Path</label>
<input id="path" placeholder="/graph/nodes">
<label for="q">Query (optional)</label>
<input id="q" placeholder="q=hbm">
<button id="go" onclick="pull()">Pull to table</button>
<p id="status" class="hint"></p>
<script>
var CAT = [];
function fill() {
google.script.run.withSuccessHandler(function (rows) {
CAT = rows || [];
var sel = document.getElementById('ep');
sel.innerHTML = '<option value="">— choose an endpoint —</option>';
CAT.forEach(function (e, i) {
var o = document.createElement('option');
o.value = String(i); o.textContent = '[' + e.dataset + '] ' + e.path;
sel.appendChild(o);
});
}).entityEndpointCatalog();
}
document.getElementById('ep').addEventListener('change', function () {
var e = CAT[this.value];
if (!e) return;
var ex = String(e.example || e.path);
var qi = ex.indexOf('?');
document.getElementById('path').value = qi >= 0 ? ex.slice(0, qi) : ex;
document.getElementById('q').value = qi >= 0 ? ex.slice(qi + 1) : '';
var ph = /\{[^}]*\}/.test(document.getElementById('path').value);
document.getElementById('status').textContent = (e.summary || '') + (ph ? ' ⚠ replace the {…} in the path with a real value' : '');
});
function pull() {
var path = document.getElementById('path').value.trim();
var q = document.getElementById('q').value.trim();
if (!path) { document.getElementById('status').textContent = 'Enter a path first.'; return; }
if (/\{[^}]*\}/.test(path)) { document.getElementById('status').textContent = 'Replace the {…} placeholder in the path with a real value first.'; return; }
var btn = document.getElementById('go'); btn.disabled = true;
document.getElementById('status').textContent = 'Pulling…';
google.script.run
.withSuccessHandler(function (r) { btn.disabled = false; document.getElementById('status').textContent = 'Wrote ' + r.rows + ' rows × ' + r.cols + ' cols' + (r.capped ? ' (capped)' : '') + '.'; })
.withFailureHandler(function (e) { btn.disabled = false; document.getElementById('status').textContent = 'THE ENTITY: ' + e.message; })
.entityImportToActiveCell(path, q);
}
fill();
</script>
</body>
</html>
Microsoft Excel — Power Query
Home ▸ Manage Parameters ▸ New: nameEntityApiKey, type Text, value = your key.Data ▸ Get Data ▸ From Other Sources ▸ Blank Query, thenAdvanced Editor.- Paste the query below, edit
Path/Params, andClose & Load.
// THE ENTITY Data — Excel via Power Query (Data > Get Data > Blank Query > Advanced Editor).
// FIRST: Home > Manage Parameters > New: Name = EntityApiKey, Type = Text, Current Value = your entk_live_ key.
// Then paste this, edit "Path" and "Params", and Close & Load. Refreshes on Excel desktop (Windows/Mac).
let
BaseUrl = "https://joinentity.com/api/v1", // keep this a LITERAL (dynamic bits go in RelativePath/Query)
Path = "/smart-money/funds", // <- change me
Params = [ scores = "true" ], // <- endpoint query params, e.g. [ q = "hbm" ]
PageSize = 200,
GetPage = (offset as number) as record =>
let
Response = Web.Contents(BaseUrl, [
RelativePath = Path,
Query = Record.Combine({ Params, [ limit = Text.From(PageSize), offset = Text.From(offset) ] }),
Headers = [ Authorization = "Bearer " & EntityApiKey ],
ManualStatusHandling = {400, 401, 403, 404, 429, 500, 503}
]),
Body = try Json.Document(Response) otherwise [ ok = false, error = "unavailable" ]
in
Body,
Page0 = GetPage(0),
Ok0 = Record.FieldOrDefault(Page0, "ok", false),
HasData0 = Record.HasFields(Page0, "data"), // distinguish data:null (degraded) from a non-enveloped body (e.g. /whoami)
Data0 = Record.FieldOrDefault(Page0, "data", null),
Meta0 = Record.FieldOrDefault(Page0, "meta", null),
Note0 = if Meta0 = null then "" else Record.FieldOrDefault(Meta0, "reason", Record.FieldOrDefault(Meta0, "note", "")),
// Page through offset until hasMore = false. A "stop" flag ends the loop on a
// clean finish (that sentinel is NOT emitted), while a page fetched mid-loop
// that comes back ok=false IS emitted, so a later-page error can be surfaced
// instead of silently truncating.
Pages = List.Generate(
() => [ off = 0, body = Page0, stop = false ],
each [stop] = false,
each
let
pg = Record.FieldOrDefault([body], "page", null),
more = Record.FieldOrDefault([body], "ok", false) = true and pg <> null and Record.FieldOrDefault(pg, "hasMore", false) = true,
next = if pg = null then [off] + PageSize else Record.FieldOrDefault(pg, "nextOffset", [off] + PageSize)
in
if more then [ off = next, body = GetPage(next), stop = false ]
else [ off = -1, body = [ ok = true, data = null ], stop = true ],
each [body]
),
ErrPage = List.First(List.Select(Pages, each Record.FieldOrDefault(_, "ok", false) <> true), null),
// A LATER page that came back degraded (ok:true, data:null) also truncates —
// surface it rather than silently returning only the earlier pages.
TruncPage = List.First(List.Select(List.Skip(Pages, 1), each Record.FieldOrDefault(_, "ok", false) = true and Record.FieldOrDefault(_, "data", null) = null), null),
// data is a list (collection endpoints) or a single record (detail/exhibit).
// Defensive unwrap ONLY for a genuine nested paginated collection: a record
// that carries a top-level 'page' AND is a PURE single-list wrapper. A record
// with a list ALONGSIDE other fields (e.g. census {latest,trend,…}) is a
// detail object → one row, never collapsed to just the list.
Rows = List.Combine(List.Transform(Pages, each
let
d = Record.FieldOrDefault(_, "data", null),
pg = Record.FieldOrDefault(_, "page", null),
names = if d <> null and d is record then Record.FieldNames(d) else {},
lists = if d <> null and d is record then List.Select(Record.FieldValues(d), each _ is list) else {}
in
if d = null then {}
else if d is list then d
else if pg <> null and List.Count(names) = 1 and List.Count(lists) = 1 then lists{0}
else { d })),
// Columns = UNION of field names across ALL rows (a field that first appears
// on a later row still becomes a column).
Cols = List.Distinct(List.Combine(List.Transform(Rows, each if _ is record then Record.FieldNames(_) else { "value" }))),
Result =
if Ok0 <> true then error "THE ENTITY: " & Record.FieldOrDefault(Page0, "error", "request failed")
// A non-enveloped OK body (e.g. /whoami) has no 'data' field — show its top-level fields, don't cry "unavailable".
else if not HasData0 then Table.FromRecords({ Record.RemoveFields(Page0, { "ok" }) })
else if Data0 = null then error "THE ENTITY: data unavailable" & (if Note0 <> "" then " (" & Note0 & ")" else "")
else if ErrPage <> null then error "THE ENTITY: pagination stopped — " & Record.FieldOrDefault(ErrPage, "error", "a later page failed")
else if TruncPage <> null then error "THE ENTITY: pagination stopped — a later page returned no data (degraded)"
else if List.Count(Rows) = 0 then #table({ "result" }, {})
else if List.First(Rows) is record then Table.FromRecords(Rows, Cols, MissingField.UseNull)
else Table.FromList(Rows, Splitter.SplitByNothing(), { "value" })
in
Result
Power Query refreshes on Excel for Windows and Mac (desktop). Excel for the web won’t refresh an authenticated source, so use the desktop app there — or the in-cell add-in below.
Microsoft Excel — in-cell =ENTITY.QUERY() beta
A self-hosted Office add-in gives Excel a live cell function, like Sheets. It’s sideloaded from our manifest (no Office Store install), works on Excel for Windows, Mac and the web, and stores your key outside the workbook.
- Save the manifest: joinentity.com/excel/manifest.xml.
- Sideload it (Microsoft’s steps): Excel for web →
Add-ins ▸ Upload My Add-in; Windows → a shared-folder catalog; Mac → theweffolder. - Open the
THE ENTITY Databutton on the Home tab, save yourentk_live_key.
=ENTITY.QUERY("/smart-money/funds","scores=true")
=ENTITY.QUERY("/graph/nodes","q=hbm")Beta — self-hosted and sideloaded, so please try it and tell us what breaks. Power Query is the supported path today; Excel custom functions require a Microsoft 365 subscription (not volume-licensed Office 2021 or iPad).
Bulk export
On Pro and Scale, one call returns a whole collection as CSV or NDJSON — the shape you point a warehouse’s external table, a COPY, or Power Query at, instead of paging the REST API thousands of times. Same key, gated on the bulksurface and the resource’s dataset scope; exactly one call is metered per pull.
# the whole graph as CSV
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/bulk?resource=graph/nodes&format=csv"
# smart-money consensus as NDJSON
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/bulk?resource=smart-money/consensus&format=ndjson"
# list what's exportable
curl -H "Authorization: Bearer $ENTITY_KEY" "https://joinentity.com/api/v1/bulk"Exportable resources:
| resource | dataset |
|---|---|
graph/nodes | graph |
graph/edges | graph |
graph/relations | graph |
graph/facilities | graph |
graph/chokepoints | graph |
graph/layers | graph |
smart-money/funds | smart-money |
smart-money/consensus | smart-money |
A pull is capped at 100,000 rows (the response carries x-entity-capped: true and x-entity-rowswhen so). Per-entity time-series (a company’s insider trades, short interest, one fund’s holdings) stay on the paginated REST endpoints above, where you filter first.
Errors
Failures return { ok: false, error } with a stable error string. The rate/quota headers are echoed on failures too.
| Status | error | Condition |
|---|---|---|
| 401 | missing_key | No Authorization: Bearer (or x-api-key) header on the request. |
| 401 | invalid_key | The key is unknown or has been revoked. |
| 403 | inactive_addon | The key's account has no active THE ENTITY Data subscription. |
| 403 | surface_not_in_tier | The tier does not include the requested surface (e.g. bulk on Starter). |
| 403 | scope_denied | The key's scopes exclude this surface + dataset. |
| 429 | rate_limited | The per-minute rate ceiling was exceeded (see retry-after). |
| 429 | quota_exceeded | The monthly call allowance is exhausted (resets at x-quota-reset). |
| 503 | unavailable | Metering infrastructure was unreachable — the request fails closed, never an unmetered grant. |
A read that throws server-side does NOT 500: it degrades to 200 with { ok: true, data: null, meta: { degraded: true } } and an x-degraded: 1 header, so a transient outage never leaks internals or looks like a confident empty.
Honesty notes
- as_of vs detected_at.
as_ofis the data's own date (a filing/trade/settlement/quarter date);detected_atis when we ingested it. They are separate fields and never collapsed into one. - “Of N tracked funds.” Smart-money counts are of the tracked elite roster (
meta.tracked_funds), not institutions at large. The company-census endpoint is the exception — it reports the full 13F filer denominator, labeled as such. - Archive vs live. The tape and clone book are quarter-end reconstructions, flagged
display_only/source: archive— never presented as real-time. - Cited, not asserted. Chain mentions and extracted metrics carry
source: cited; every mention is a verbatim, source-linked quote, and single-point series are suppressed by the reader.