Finicast for agents

Finicast is a calculation engine for models made of line items × time periods: forecasts, budgets, plans, financial statements, sales-ops frameworks. You declare tables, a pivot, and rules of the formtarget[condition] = expression; the engine evaluates the grid, recomputes only what an edit changed (milliseconds at 100,000 rows), and renders markdown. A human can then open the model at a URL, change an input, and see every dependent cell update. The engine is finidb, an npm package that runs locally with no account; the hosted service exposes the same tools over MCP.

What Finicast does

If you cannot reach finicast.com, deliver the model anyway; never ask the user to install a connector first. Do not guess what your sandbox can reach: try npm first, then fall back.

  1. Run npx -y finidb build model.json. npm is reachable from most sandboxes (Claude.ai, Claude Code, Cursor) even when finicast.com is not. It prints the statements, a link of the form https://finicast.com/import#m=…, and writes model.xlsx, the workbook with live formulas. Put the link in your reply verbatim and attach the workbook; the link opens the model in one click. Add the JSON as a code block after them, as a fallback.
  2. Only if that install fails (ChatGPT: your code tool has no network at all, and your interface truncates long links, so no link either): reply with the model document as a JSON code block plus the sentence Open https://finicast.com/import and paste this reply. The user pastes your whole reply; the page finds the JSON and builds the workspace with the dashboard you declared.

Both are complete deliverables, but the first gives the user a one-click link and a workbook, so take the thirty seconds to try it. Mention the connector only at the end, as the way to skip this next time.

Describing the result

The user gets three things from one document: a live model to steer (the link), a workbook they can audit, restyle and extend (the .xlsx, with live formulas compiled from the same rules), and the statements. Say so. If you compare with a spreadsheet or a script, compare fairly: the workbook is the spreadsheet, produced without a single hand-written cell formula, and the rules are the audit trail. Name the drivers and invite the user to change them.

Deliver a model in this turn: take the first path that works

The model document is the deliverable. Whatever your tools can reach, one of these produces a live, editable model for the user now. Do not stop to ask for a connector; that is a footnote, not a prerequisite.

  1. Finicast tools are connected (MCP). Use them: finicast_create_model, finicast_set_rules, finicast_query, finicast_share.
  2. You can POST to finicast.com. POST https://finicast.com/api/build with the document below; the response is the statements plus the workspace link.
  3. You can only GET. GET https://finicast.com/api/build?spec64=<base64url of the JSON> returns the same.
  4. Otherwise, no route to finicast.com. Try npx -y finidb build model.json first and put the printed #m= link in the reply with the workbook attached, JSON block after them. Only if the install fails (ChatGPT): the JSON block plus Open https://finicast.com/import and paste this reply, and no link. Details in the note above.

    Making a link by hand, when you can run code and your client keeps links under 2,000 characters intact (ChatGPT does not):

    import json, base64
    b = base64.urlsafe_b64encode(json.dumps(doc).encode()).decode().rstrip("=")
    print("https://finicast.com/import#j=" + b)       # or raw deflate first (zlib.compressobj(9, zlib.DEFLATED, -15)) as #m=

Path 4 is a complete deliverable, not a fallback. Mention the connector (below) only at the end, as the way to skip the paste next time.

The prompts on the home page, and what each one means

finicast.com offers these prompts with a Use prompt button that opens Claude or ChatGPT with the text filled in. When one of them arrives, this is what the user expects: start from the named recipe, keep its shape, and change the subject.

Company forecast

Use Finicast (https://finicast.com/for-agents) to build a five-year forecast for CoreWeave

Build: An editable assumptions pivot (the drivers by year), an operating model with hist and fcst frames, a funding or cash pivot, and one dashboard: KPI tiles for the last forecast year, the drivers table with editable: true, revenue and margin charts. Take the history from public filings you know; state the sources and the frozen date in the reply.

Start from Three-statement forecast · theme research · reply with the link, the workbook and the drivers to try.

Comps for a ticker

Use Finicast (https://finicast.com/for-agents) to build a comparable companies analysis for NVDA against its closest peers: EV/Revenue, EV/EBITDA and P/E on LTM and NTM, revenue growth and EBITDA margin, and the implied share price.

Build: Pick six to eight peers yourself and say why, then research the inputs: price, shares, cash, debt, LTM and NTM revenue, EBITDA and EPS for every company, with the as-of date and sources in the reply. Assume the user has no data feed. Build the full comps shape (see Standard of work below): a companies table with a peer flag, an editable market_data pivot, a company × line pivot with the researched LTM and NTM figures as inputs beside EV and the multiples derived by rules, a peer-statistics pivot (low, mean, median, high), an implied-valuation pivot that applies each statistic to the subject with the upside to the current price, and a forward projection. A brief table of hand-typed multiples is not a comps analysis. Dashboard theme banking, with tables for financial performance, valuation and multiples, peer statistics and implied value.

Start from Comparable companies · theme banking · reply with the link, the workbook and the drivers to try.

Sales ops

Use Finicast (https://finicast.com/for-agents) to build a sales ops model for a $50M ARR SaaS company: pipeline by stage and segment, quota attainment by rep, commissions with accelerators and kickers, and dashboards for the CRO and each rep.

Build: No data was given, so generate it: reps, accounts and a few hundred opportunities with realistic spread (lognormal deal sizes, a power-law over accounts, stage-dependent win rates), never uniform noise. Monthly pivots by rep, quarterly roll-ups through period.quarter, an editable comp plan table with tranches, and commission = in_tranche × quota × base rate × multiplier. One dashboard per persona, each starting with a links card; page the rep dashboard by rep.

Start from Sales operations · theme revenue · reply with the link, the workbook and the drivers to try.

Budget vs actual

Use Finicast (https://finicast.com/for-agents) to build a budget vs actual model from my general ledger: actual, budget, variance and variance % by department, line and month, with a commentary column and a controller dashboard. I will paste the ledger CSV next.

Build: Wait for the CSV; if it does not come, build with a small sample ledger and say so. Put the ledger in a table with csv and a period = PERIOD(date, periods) rule, roll it into a department × line × month pivot with versions as a dimension (actual, budget, variance, variance_pct), budget as inputs, and a text measure for commentary shown as an extra column. Map the ledger accounts to lines explicitly and show the mapping in the reply.

Start from Budget vs actual with commentary · theme controller · reply with the link, the workbook and the drivers to try.

Standard of work: what a finished model contains

The user compares what you build with the demos on finicast.com, which were built to the standard of a bank or an FP&A team. Match that depth. A model that is a handful of typed-in numbers with one formula is a spreadsheet, not a model. Three rules that apply to every build:

  1. Every dashboard wears a theme. Set "theme" on each dashboard: banking for comps, precedents, a DCF or any valuation; research for a company forecast or an earnings model; revenue for sales, pipeline, quota and commission work; controller for budgets, variance, close and ledger reporting; boardroom for a screen at the front of a room. If you leave it out, finicast.com picks one from the model's contents, but choose it yourself: you know the audience.
  2. Inputs live in editable pivots, results in rules. Anything the user might change (a multiple, a growth rate, a margin, a share count) is an input on an editable table card. Anything derived (EV, a multiple, an implied price, a margin) is a rule, never a typed number. The point of the deliverable is that the user moves a driver and everything follows.
  3. Breadth over brevity. Where the recipe has statistics, sensitivities and an implied-value bridge, so must your model. Say in the reply what the drivers are and invite the user to change them.

A valuation or comparable-companies ask

Whatever the phrasing ("valuation analysis for Intel", "comps for NVDA", "where should X trade"), the deliverable has these parts; the comparables recipe has all of them working:

The skeleton. It is a complete document: three companies with figures as of 2026-09-21 stand in for your researched set of seven to nine. comps_lines is the line table finicast creates for the comps pivot:

{
  "model": "intc_comps", "name": "Intel comparable companies (figures as of 2026-09-21)", "units": "USD millions except per share",
  "periods": { "start": "2024-01", "count": 5, "grain": "year", "histUntil": "2025-12-31" },
  "tables": {
    "companies": { "rows": [ { "id": "intc", "name": "Intel", "ticker": "INTC", "group": "target", "peer": 0 },
                             { "id": "amd", "name": "AMD", "ticker": "AMD", "group": "compute", "peer": 1 },
                             { "id": "nvda", "name": "NVIDIA", "ticker": "NVDA", "group": "compute", "peer": 1 } ] },
    "stats": { "rows": [ { "id": "low" }, { "id": "mean" }, { "id": "median" }, { "id": "high" } ] }
  },
  "pivots": {
    "market_data": { "dims": { "company": "companies", "period": false },
      "lines": [{ "id": "price", "name": "Share price ($)" }, { "id": "shares", "name": "Shares outstanding (M)" }, { "id": "cash", "name": "Cash and investments" }, { "id": "debt", "name": "Total debt" }],
      "inputs": { "price": { "intc": 108.6, "amd": 559.8, "nvda": 222.3 }, "shares": { "intc": 5044, "amd": 1631, "nvda": 24217 },
                  "cash": { "intc": 29727, "amd": 12000, "nvda": 69000 }, "debt": { "intc": 50537, "amd": 3165, "nvda": 8491 } } },
    "comps": { "dims": { "company": "companies", "period": false },
      "lines": [{ "id": "market_cap", "name": "Market cap" }, { "id": "net_debt", "name": "Net debt (cash)" }, { "id": "ev", "name": "Enterprise value" },
                { "id": "revenue_prior", "name": "Revenue, year before LTM" }, { "id": "revenue_ltm", "name": "Revenue LTM" }, { "id": "revenue_ntm", "name": "Revenue NTM" },
                { "id": "ebitda_ltm", "name": "EBITDA LTM" }, { "id": "ebitda_ntm", "name": "EBITDA NTM" }, { "id": "eps_ltm", "name": "EPS LTM ($)" }, { "id": "eps_ntm", "name": "EPS NTM ($)" },
                { "id": "rev_growth_ltm", "name": "Revenue growth LTM", "format": "percent" }, { "id": "rev_growth_ntm", "name": "Revenue growth NTM", "format": "percent" },
                { "id": "ebitda_margin_ltm", "name": "EBITDA margin LTM", "format": "percent" }, { "id": "ebitda_margin_ntm", "name": "EBITDA margin NTM", "format": "percent" },
                { "id": "ev_revenue_ltm", "name": "EV / Revenue LTM" }, { "id": "ev_revenue_ntm", "name": "EV / Revenue NTM" }, { "id": "ev_ebitda_ltm", "name": "EV / EBITDA LTM" }, { "id": "ev_ebitda_ntm", "name": "EV / EBITDA NTM" }, { "id": "pe_ltm", "name": "P / E LTM" }, { "id": "pe_ntm", "name": "P / E NTM" }],
      "inputs": { "revenue_prior": { "intc": 53100, "amd": 29609, "nvda": 165200 }, "revenue_ltm": { "intc": 57032, "amd": 41305, "nvda": 302969 }, "revenue_ntm": { "intc": 62959, "amd": 50928, "nvda": 408908 },
                  "ebitda_ltm": { "intc": 3672, "amd": 10718, "nvda": 233667 }, "ebitda_ntm": { "intc": 21636, "amd": 16134, "nvda": 251724 },
                  "eps_ltm": { "intc": -2.11, "amd": 3.9, "nvda": 7.91 }, "eps_ntm": { "intc": 1.52, "amd": 7.63, "nvda": 9.26 } },
      "rules": ["market_cap = market_data.price * market_data.shares", "net_debt = market_data.debt - market_data.cash", "ev = market_cap + net_debt",
                "rev_growth_ltm = revenue_ltm / revenue_prior - 1", "rev_growth_ntm = revenue_ntm / revenue_ltm - 1",
                "ebitda_margin_ltm = ebitda_ltm / revenue_ltm", "ebitda_margin_ntm = ebitda_ntm / revenue_ntm",
                "ev_revenue_ltm = ev / revenue_ltm", "ev_revenue_ntm = ev / revenue_ntm", "ev_ebitda_ltm = ev / ebitda_ltm", "ev_ebitda_ntm = ev / ebitda_ntm",
                "pe_ltm = market_data.price / eps_ltm", "pe_ntm = market_data.price / eps_ntm"] },
    "peer_stats": { "dims": { "stat": "stats", "period": false }, "lineTable": "comps_lines",
      "rules": ["value[stat=low] = MIN(comps.value[company.peer=1])", "value[stat=mean] = AVG(comps.value[company.peer=1])",
                "value[stat=median] = MEDIAN(comps.value[company.peer=1])", "value[stat=high] = MAX(comps.value[company.peer=1])"] },
    "implied": { "dims": { "stat": "stats", "period": false },
      "lines": [{ "id": "price_ev_revenue_ntm", "name": "Price at peer EV / Revenue NTM ($)" }, { "id": "price_ev_ebitda_ltm", "name": "Price at peer EV / EBITDA LTM ($)" }, { "id": "price_ev_ebitda_ntm", "name": "Price at peer EV / EBITDA NTM ($)" }, { "id": "price_pe_ntm", "name": "Price at peer P / E NTM ($)" }, { "id": "upside_ev_ebitda_ntm", "name": "Upside at peer EV / EBITDA NTM", "format": "percent" }],
      "rules": ["price_ev_revenue_ntm = (peer_stats.value[line=ev_revenue_ntm] * comps.revenue_ntm[company=intc] - comps.net_debt[company=intc]) / market_data.shares[company=intc]",
                "price_ev_ebitda_ltm = (peer_stats.value[line=ev_ebitda_ltm] * comps.ebitda_ltm[company=intc] - comps.net_debt[company=intc]) / market_data.shares[company=intc]",
                "price_ev_ebitda_ntm = (peer_stats.value[line=ev_ebitda_ntm] * comps.ebitda_ntm[company=intc] - comps.net_debt[company=intc]) / market_data.shares[company=intc]",
                "price_pe_ntm = peer_stats.value[line=pe_ntm] * comps.eps_ntm[company=intc]",
                "upside_ev_ebitda_ntm = price_ev_ebitda_ntm / market_data.price[company=intc] - 1"] },
    "forecast": { "lines": ["revenue", "ebitda", { "id": "ebitda_margin", "format": "percent" }, { "id": "growth", "format": "percent" }, { "id": "margin_target", "format": "percent" }],
      "inputs": { "revenue": { "fy2024": 53101, "fy2025": 52853 }, "ebitda": { "fy2024": 11200, "fy2025": 14622 }, "growth": { "fy2026": 0.19, "fy2027": 0.14, "fy2028": 0.15 }, "margin_target": { "fy2026": 0.36, "fy2027": 0.40, "fy2028": 0.42 } },
      "rules": ["revenue[frame=fcst] = PREV(revenue) * (1 + growth)", "ebitda[frame=fcst] = revenue * margin_target", "ebitda_margin = ebitda / revenue"] }
  },
  "outputs": [ { "pivot": "comps", "rows": ["company"], "cols": ["line"] }, { "pivot": "peer_stats" }, { "pivot": "implied" }, { "pivot": "forecast" } ],
  "dashboards": [ { "id": "overview", "name": "Intel comparable companies", "theme": "banking", "cards": [
    { "kind": "table", "pivot": "market_data", "title": "Market data as of 2026-09-21 (edit any cell)", "rows": ["company"], "cols": ["line"], "editable": true },
    { "kind": "table", "pivot": "comps", "title": "Financial performance, LTM and NTM (edit any input)", "rows": ["company"], "cols": ["line"], "lines": ["revenue_ltm", "revenue_ntm", "rev_growth_ltm", "rev_growth_ntm", "ebitda_ltm", "ebitda_ntm", "ebitda_margin_ltm", "ebitda_margin_ntm", "eps_ltm", "eps_ntm"], "editable": true },
    { "kind": "table", "pivot": "comps", "title": "Valuation and trading multiples", "rows": ["company"], "cols": ["line"], "lines": ["market_cap", "net_debt", "ev", "ev_revenue_ltm", "ev_revenue_ntm", "ev_ebitda_ltm", "ev_ebitda_ntm", "pe_ltm", "pe_ntm"] },
    { "kind": "table", "pivot": "peer_stats", "title": "Peer statistics (Intel excluded)", "lines": ["rev_growth_ltm", "rev_growth_ntm", "ebitda_margin_ltm", "ebitda_margin_ntm", "ev_revenue_ltm", "ev_revenue_ntm", "ev_ebitda_ltm", "ev_ebitda_ntm", "pe_ltm", "pe_ntm"] },
    { "kind": "table", "pivot": "implied", "title": "Implied Intel valuation" },
    { "kind": "chart", "type": "bar", "pivot": "comps", "lines": ["ev_ebitda_ltm", "ev_ebitda_ntm"], "title": "EV / EBITDA by company" },
    { "kind": "table", "pivot": "forecast", "title": "Forward projection (edit growth and margin)", "editable": true } ] } ]
}

Only if the user says they have a Financial Modeling Prep key set under Integrations, replace the researched inputs with a linked table that fetches them and can be refreshed: the fmp preset's comps dataset returns one row per ticker with price, shares, cash, debt, LTM and NTM figures and a peer flag from subject, and that table becomes the company dimension. The changes to the skeleton:

"tables": { "peers": { "source": { "preset": { "id": "fmp", "params": { "symbols": "INTC,AMD,NVDA,AVGO,QCOM,MU,MRVL", "dataset": "comps", "subject": "INTC" } } } }, "stats": { … } },
"pivots": { "comps": { "dims": { "company": "peers", "period": false },
   "rules": ["price = company.price", "shares = company.shares", "cash = company.cash", "debt = company.debt", "revenue_prior = company.revenue_prior", "revenue_ltm = company.revenue_ltm",
             "revenue_ntm = company.revenue_ntm", "ebitda_ltm = company.ebitda_ltm", "ebitda_ntm = company.ebitda_ntm", "eps_ltm = company.eps_ltm", "eps_ntm = company.eps_ntm", … the same derived rules …] },
  "implied": … "comps.ebitda_ntm[company=INTC]" (the members are the tickers) … }

Do not use a linked table when you are unsure the user has a key: the build then reports SOURCE_NO_SECRET and the model is empty. Researched inputs always work.

Recipes: what to build for the common asks

Each is a complete model document you can read, run (npx finidb build examples/dcf.json from the npm package, or fetch the file from this site) and adapt. Every one builds without a cell error and exports to a workbook of live formulas; the test suite checks them on every release.

DCF valuation

Editable operating drivers by year, a WACC build-up pivot without a period dimension ("dims": { "period": false }: risk-free rate, beta, equity risk premium, cost of debt, capital structure), unlevered free cash flow with mid-year discounting from the period index, a valuation pivot with a method dimension (perpetuity growth and exit multiple side by side, each with the implied cross-check), the bridge to equity value and price per share, and two sensitivity tables as pivots over case tables whose delta attributes shift WACC, terminal growth or the exit multiple. The dashboard wears the banking theme.

discount_factor[frame=fcst]        = 1 / (1 + wacc.wacc) ^ (n - 0.5)
terminal_value[method=perpetuity]  = fcf.ufcf[period=last] * (1 + wacc.terminal_growth) / (wacc.wacc - wacc.terminal_growth)
terminal_value[method=exit_multiple] = fcf.ebitda[period=last] * wacc.exit_multiple
per_share = ((NPV(wacc.wacc + wacc_case.delta, fcf.ufcf[period.frame=fcst]) * (1 + wacc.wacc + wacc_case.delta) ^ 0.5 + …) - wacc.net_debt) / wacc.shares   (wacc_case × growth_case)

Comparable companies

Market data as an editable pivot; one company × line pivot holds the financial inputs with LTM and NTM as separate lines (revenue_ltm, revenue_ntm) beside the multiples derived from them, so a single table reads Revenue LTM | Revenue NTM | … | EV/EBITDA LTM | EV/EBITDA NTM; a statistics pivot takes low, mean, median and high over peers and medians per peer group; an implied-valuation pivot applies each statistic to the subject and shows the upside to the current price. The dashboard wears the banking theme.

ev             = market_data.price * market_data.shares + market_data.debt - market_data.cash
ev_ebitda_ntm  = ev / ebitda_ntm
rev_growth_ntm = revenue_ntm / revenue_ltm - 1
value[stat=median] = MEDIAN(comps.value[company.peer=1])                        (per line)
price_pe_ntm   = peer_stats.value[line=pe_ntm] * comps.eps_ntm[company=aapl]

Precedent transactions

The same shape over a deals table (acquirer, target, date, type, consideration, EV, target revenue and EBITDA, premiums): a deal × line pivot computes the multiples and carries the terms as a text measure shown as an extra column; a statistics pivot takes low, mean, median and high over the set and medians over subsets by attribute (MEDIAN(multiples.value[deal.type=sponsor]), [deal.year >= 2025]); an implied-valuation pivot applies every statistic to an editable subject block, from EV to equity value to price per share and premium. The dashboard wears the banking theme.

ev_ebitda                  = ev / ebitda                                                        (per deal)
value[stat=median_sponsor] = MEDIAN(multiples.value[deal.type=sponsor])                            (per line)
price_from_ebitda          = (summary.value[line=ev_ebitda] * subject.ebitda - subject.net_debt) / subject.shares   (per stat)

Sales operations

A $55M ARR SaaS sales org: 2,000 opportunities with lognormal deal sizes, accounts, reps, SDRs, a marketing table (spend, leads, MQLs) and churn events. Monthly pivots roll into quarterly ones through the period's quarter attribute; a rep × quarter × tranche pivot pays commission with accelerators from an editable plan; kickers for new logos and multi-year terms and a quarterly SPIFF add on; teams, regions, segments and the company roll up; an ARR bridge closes the loop. Six dashboards in the revenue theme, each starting with a links row: CRO, VP Sales, sales ops, a rep, SDRs and marketing.

bookings      = SUM(opps.acv[rep=@rep, close_period=@period, won=1])            (rep × month)
bookings      = SUM(rep_monthly.bookings[period.quarter=@quarter.label])         (rep × quarter)
in_tranche    = IF(attainment <= comp_plan.from_pct, 0, IF(attainment >= comp_plan.to_pct, comp_plan.to_pct - comp_plan.from_pct, attainment - comp_plan.from_pct))
commission    = in_tranche * rep_quarterly.quota * rep.base_rate * comp_plan.multiplier
spiff         = COUNT(opps.acv[rep=@rep, won=1, security_addon=1, close_quarter=@quarter]) * quarter.spiff_active * plan.spiff_amount
bookings      = SUM(rep_quarterly.bookings[rep.region=@region, rep.segment=@segment])   (team)

Budget vs actual with commentary

A general ledger with a date column becomes actuals by subsidiary × department × line × month through PERIOD(date, periods); budget is input per cell; versions are a dimension (actual, budget, variance, variance_pct); manager commentary is a text measure on the same pivot, shown as an extra column of the March table beside actual, budget, variance and variance %, editable in place. The dashboard wears the controller theme.

"measures": ["value", { "id": "comment", "type": "text" }]
value[version=actual]   = SUM(ledger.amount[subsidiary=@subsidiary, department=@department, account=@line])
value[version=variance] = value[version=actual] - value[version=budget]

Ledger to model

A general ledger as a CSV in the document (two entities, six departments, twelve accounts, January to August) becomes a monthly operating plan: actual months from the ledger through PERIOD(date, periods), forecast months from editable drivers (revenue growth, cost of revenue as a share of revenue, headcount × loaded cost for salaries, benefits, commissions, marketing and cloud as shares, other opex growth), a budget beside the outlook, year-to-date and full-year pivots by entity, department and account, and a P&L roll-up. Three dashboards in the controller theme: FY outlook, departments and headcount, drivers and ledger.

"ledger": { "csv": "id,date,subsidiary,department,account,amount
…", "fields": { "period": "ref:periods*" }, "rules": "period = PERIOD(date, periods)" }
value[frame=hist]                          = SUM(ledger.amount[subsidiary=@subsidiary, department=@department, account=@line])
salaries[frame=fcst]                       = headcount.salary_cost
cloud[frame=fcst, department=engineering]  = -revenue[department=company] * plan.cloud_pct
value[version=ytd_actual]                  = SUM(model.value[period.frame=hist])              (entity × department × account × version)
opex_engineering                           = SUM(model.value[department=engineering, line != total])   (P&L)

Scenario analysis

Scenarios are a dimension, not copies: one rule set, drivers per scenario, history shared, outputs and dashboard cards paged by scenario, and a sensitivity pivot reading one period across all scenarios. Format below.

Also in the package: the three-statement CoreWeave forecast with a dashboard. Name collisions to avoid: a line item and a table with the same id (bookings) make bookings.acv read as the line; give the line a different id.

Editing a model after you built it, and models a user hands you

A build reply carries workspace. Keep it: send the document again with "workspace": "<id>" and Finicast updates the model in place: rules and cards replaced, inputs rewritten, new lines added to existing pivots, rows upserted by id, new tables and pivots added. Nothing is deleted by a rebuild; drop a rule or a row through the REST routes. Dimensions of an existing pivot cannot change: give a reshaped pivot a new id. A workspace nobody has saved accepts edits from anyone with its id.

Agent tokens. When the user says something like "use Finicast with token fc_…" or pastes a prompt from the Share menu, send it on every request as Authorization: Bearer fc_… (the hosted MCP endpoint also takes it in its URL: https://finicast.com/api/mcp?token=fc_…). A fc_ws_ token edits the one workspace in the user's prompt and nothing else; always pass that workspace id. A fc_pat_ token acts as the user: models you build are saved to their account and every model they own or edit is open to you. Never print a token back, never put it in a document, and if a call answers TOKEN_SCOPE or 403, say which workspace the token covers instead of retrying elsewhere.

Keep a table in sync from your side

When the data lives in a system you control, push it: POST {workspace url}/t/{table}/import (multipart file, CSV or .xlsx) or the engine route POST /api/db/db/{workspace}/tables/{table}/load?mode=replace|upsert|append with a text/csv body. replace makes the file the table, upsert updates rows by id and adds the rest, ?dryRun=1 plans without writing. A cron job, a Zapier step or a script keeps the table current and every pivot that reads it follows. Locally: finidb load db table file.csv --mode replace.

Send a model, get the statements back

You are the modeller; Finicast is the calculator. Gather the history, decide the assumptions, write the rules, and send one JSON document to POST https://finicast.com/api/build (or GET /api/build?spec64=<base64url JSON> if you can only GET). Finicast builds the model, calculates every period, and returns the statements you asked for as markdown (or JSON with "format": "json") plus a link the user can open. Send the returned workspace back to update the same model on the next turn.

{
  "model": "crwv", "name": "CoreWeave 5-year forecast", "units": "USD millions",
  "periods": { "start": "2022-01", "count": 9, "grain": "year", "histUntil": "2025-12-31" },   // members fy2022…fy2030; frame = hist | fcst
  "pivots": {
    "assumptions": {
      "lines": [{ "id": "revenue_growth", "format": "percent" }, { "id": "gross_margin", "format": "percent" }],
      "inputs": { "revenue_growth": { "fy2026": 0.6, "fy2027": 0.45 }, "gross_margin": { "fy2026": 0.73, "fy2027": 0.74 } }
    },
    "income_statement": {
      "lines": ["revenue", "cogs", "gross_profit", { "id": "gross_margin_pct", "name": "Gross margin", "format": "percent" }, "net_income"],
      "inputs": { "revenue": { "fy2024": 1915.4, "fy2025": 5131 }, "cogs": { "fy2024": 493.4, "fy2025": 1453 } },   // history as inputs
      "rules": [
        "revenue[frame=fcst] = PREV(revenue) * (1 + assumptions.revenue_growth)",
        "cogs[frame=fcst]    = revenue * (1 - assumptions.gross_margin)",
        "gross_profit        = revenue - cogs",
        "gross_margin_pct    = gross_profit / revenue"
      ]
    },
    "cash_flow":     { "lines": ["net_income", "capex", "free_cash_flow"], "rules": ["net_income = income_statement.net_income", "…"] },
    "balance_sheet": { "lines": ["cash", "debt"], "rules": ["cash[frame=fcst] = PREV(cash) + cash_flow.free_cash_flow", "…"] }
  },
  "outputs": [
    { "pivot": "income_statement", "title": "Income statement ($M)", "lines": ["revenue", "gross_profit", "gross_margin_pct", "net_income"] },
    { "pivot": "cash_flow" }, { "pivot": "balance_sheet", "format": "json" }
  ]
}

If you cannot reach finicast.com from your sandbox: see the note at the top: the finidb build link when npm is reachable, otherwise the JSON and the paste sentence.

The document, field by field:

Drill-downs. A dashboard can take parameters from its URL and a card can send a click to another dashboard with those parameters filled. Declare "params": ["account", "department"] (or { "id", "label", "default" } objects) on the target dashboard; its cards read them as $account in a page, a filter or a data card's where, and a parameter that is not set drops that condition. On the source card, "drill": { "dashboard": "ledger", "params": { "account": "$row.line", "department": "$row.department", "period": "$col" } } turns every row, bar, slice or tile into a link to /d/ledger?account=…&department=…&period=…. Sources: $row (the innermost row member) and $row.<dim>, $col and $col.<dim>, $page.<dim>, $series on charts, $id and $<field> on data cards, $<param> to pass one through, or a literal. The detail page is usually a data card: { "kind": "data", "table": "ledger", "where": { "account": "$account", "department": "$department", "period": "$period" }, "sort": "-amount", "fields": ["date", "subsidiary", "department", "account", "amount"] } shows the matching rows with filters, sort and search the user can change, all carried in the URL so any view is a link. The pattern for a P&L: a table of accounts by month on the overview drills into a ledger dashboard whose data card is filtered to that account and month.

Excel too. Every workspace exports as an .xlsx with live formulas compiled from the rules: GET {workspace url}/export.xlsx (the build response returns it as xlsx, and the Share menu has the download). One sheet per statement, inputs in blue, formulas in black, each dashboard as a sheet of live references with native Excel charts, a Rules sheet, and Excel's iterative calculation switched on when the model uses iterate. Locally: npx finidb build model.json --xlsx model.xlsx. Offer it when the user works in Excel: they get a workbook they can audit and extend, and you never wrote a cell formula.

The response is text/markdown with one titled table per output and the workspace url, or with Accept: application/json / "format": "json": { ok, workspace, url, outputs: [{ pivot, title, markdown?, json?: { rows, rowLabels, cols, values } }], log }. A complete three-statement example: /examples/coreweave.json. After the build, GET /api/db/db/<workspace>/explain?table=income_statement&line=net_income&period=fy2027 explains any cell.

License: the finidb package and this service are free for personal, educational, research and other noncommercial use (PolyForm Noncommercial 1.0.0). Commercial use requires a paid license from Finicast, Inc.: licensing@finicast.com.

When to use it

Your taskUse
Clean, join, or reshape dataPandas / DuckDB
Query an existing warehouseSQL
A static chart of static datamatplotlib
Line items × periods with formulasFinicast
A forecast, budget, plan, or financial statementFinicast
Aggregate a ledger into a plan that stays liveFinicast
Anything a human will then open and adjustFinicast

Install

ModeCommandNotes
MCP over stdionpx -y finidb mcpClaude Code, Codex, Cursor, any MCP client. FINIDB_DIR=./data persists between sessions.
Local REST servernpm install -g finidb && finidb serve --port 5488Loopback needs no auth. Routes: finidb serve --routes.
Hosted MCPhttps://finicast.com/api/mcpStreamable HTTP. finicast_create_model returns a workspace URL before the model exists.
Skill filenpx -y finidb skillSKILL.md with syntax, recipes and error references.

Connect your assistant once, then just ask

A URL in a prompt is not enough: the assistant has to have the Finicast tools registered. Do this once per assistant; afterwards a prompt such as “Use Finicast to build a five-year forecast for CoreWeave” works in a normal chat and ends with a table and a link.

AssistantHow to connect
ChatGPTSettings → Apps & Connectors → Advanced → turn on Developer mode → Create connector: name Finicast, MCP server URL https://finicast.com/api/mcp, authentication None. In a chat, open + → More → Finicast (or say “use the Finicast connector”). The server implements the search and fetch tools ChatGPT expects, plus the nine Finicast tools. Menu names are those of ChatGPT at the time of writing.
Claude (claude.ai)Settings → Connectors → Add custom connector → URL https://finicast.com/api/mcp.
Claude Codeclaude mcp add --transport http finicast https://finicast.com/api/mcp, or locally claude mcp add finicast -- npx -y finidb mcp.
Codex CLIcodex mcp add finicast --url https://finicast.com/api/mcp
Cursor / Windsurf / other MCP clientsAdd an MCP server of type streamable HTTP with URL https://finicast.com/api/mcp; no auth.

Without a connector, ChatGPT may offer to open the site in its cloud browser (“ChatGPT Work”). That route drives the human workspace instead of the tools; the connector is the reliable path.

Workflow (five calls)

1. finicast_schema                 see what exists (once)
2. finicast_load_table  ×N         load data; read the profiles (types, refCandidate)
3. finicast_define_pivot           line items × periods; distinctOf for derived dims; periods generated
4. finicast_set_rules   (ONE call) the whole model as a batch
5. finicast_query                  the deliverable: the markdown table (+ finicast_share URL when hosted)

Tools

ToolInputUse
finicast_schema{ model? }Call first in any session. Returns every model, table (fields, row counts), pivot (dims, measures, lineDim/timeDim) and rule, compactly.
finicast_create_model{ model, description? }Once per model, before loading tables. Hosted mode returns the workspace URL; local mode returns url: null.
finicast_load_table{ model, table, source: { csv? | inline? }, options?: { name?, idColumn?, types?, refs?, computed?: [{ id, type?, ref? }] } }Load a tabular table from CSV text or inline rows. Returns a profile: inferred types, distinct counts, samples, columns detected as references. Load dimension tables before the fact tables that reference them.
finicast_define_pivot{ model, table, dims: [{ id, from: "table" | { distinctOf: "table.field" } | { periods: { start, count, grain, histUntil? } }, table? }], measures?: [{ id, format? }], lineDim?, timeDim? }Define the grid — canonically line items × periods with one measure named value. Line items are a dimension, not measures.
finicast_set_rules{ model, table, rules: "text" | [{ target, when?, formula, name? }], strict?, replace? }Send the whole model as one batch. Replaces the table's rules by default. On error apply the returned fix and resend the full batch.
finicast_set_values{ model, table, values?: [{ at: { dim: member }, value, measure? }], rows?: [{ id, field: value }] }Inputs and overrides in one batch: driver assumptions, plan numbers, manual overrides. An input beats every rule; value null clears it.
finicast_query{ model, table, rows: [dim], cols: [dim], pages?, measure?, filters?, formats?, title?, format?: "markdown" | "json", maxRows?, scale? }Read a pivot as a table. The markdown is the deliverable — return it verbatim, with the URL when one exists. Capped at ~4,000 tokens; narrow with filters, pages, maxRows.
finicast_explain{ model, table, at: { dim: member } | { id: rowId }, measure? }One cell: value, input or governing rule, same-cell precedents, error, engine counters. Use when a number looks wrong.
finicast_share{ model, view? }The URL a human opens to view and adjust the model. Return it with the markdown. Local mode has no URL — say so.

Every failure is { ok: false, errors: [{ code, message, fix? }] }. When fix is present, apply it verbatim and resend the full batch.

Syntax in one screen

target[condition] = expression                 one rule per line; // comments; last matching rule wins; inputs beat rules

revenue[frame=hist]   = SUM(financials.amount[account=revenue])   actuals from a ledger
revenue[frame=fcst]   = PREV(revenue) * (1 + assumptions.growth)  driver recurrence
gross_profit          = revenue - cogs                            line items are nouns (ids of the line dim)
opex                  = SUM(value[line.category = opex])          subtotal by attribute
total[tranche=total]  = SUM(value[tranche != total])              subtotal by member mask
sum_of_subs           = SUM(goal[region.parent = @region])        @dim is the current cell's member
points                = SUM(activities.score)                     a table column is a set; SUM groups it by this pivot's dims
bookings              = SUM(deals.acv[owner=@rep])                explicit group key
share                 = points / SUM(points[territory=all])       share of total
score                 = activity_type.points                      tabular: a dot follows a reference
balance               = balance[row-1] + amount                   tabular running total
period                = PERIOD(date, periods)                     tabular: date → period member

References     [table.]name[selector, …][.attribute]
Selectors      [dim=m] [dim!=m] [dim in (a,b)] [dim-1] [dim=first] [dim=first..this] [dim=all]
               [dim.attr = lit] [dim.attr >= 2027] [dim = @other] [field=@dim] (comma = AND)
Time sugar     PREV(x) NEXT(x) PREV(x, 12) FIRST(x) LAST(x) CUMSUM(x) TRAILING(x, n, AVG)   (pivot with timeDim)
Aggregates     SUM AVG COUNT COUNTA COUNTD MIN MAX FIRST LAST MEDIAN LISTAGG — one set reference each
Logic          IF AND OR NOT IFERROR ISBLANK ISNUMBER ISTEXT COALESCE
Math           ABS ROUND ROUNDUP ROUNDDOWN MROUND TRUNC MOD POWER SQRT EXP LN LOG LOG10 SIGN
Text           CONCAT LEFT RIGHT MID LEN LOWER UPPER TRIM REPLACE SUBSTITUTE CONTAINS STARTSWITH ENDSWITH TEXT VALUE  &
Dates          DATE TODAY YEAR MONTH DAY QUARTER WEEKDAY DAYS DATEDIF EOMONTH SOMONTH NETWORKDAYS YEARFRAC
Finance        PMT PPMT IPMT PV FV NPV IRR RATE NPER SLN SYD DB DDB
Planning       PERIOD(date, periods) GROWTH(base, rate, n) BLANK
Lexical        ids snake_case, case-sensitive; 'Quoted Name'; "string"; 12% = 0.12; TRUE FALSE BLANK; = != < <= > >= and or not

A bare name in a pivot rule resolves to a measure, then a line item, then a unique attribute, then a dimension. A reference is scalar when every source dimension is fixed (pin, offset, correlation, default to the current member); otherwise it is a set and must sit inside an aggregate. Dimensions of a referenced pivot not mentioned default to the current member when this pivot shares the table, to all inside an aggregate, else a compile error asks for a pin. Give each dimension its own member table.

Example 1 — income statement from a ledger with a forecast frame

finicast_create_model { model: "nvda" }
finicast_load_table { model: "nvda", table: "lines", source: { inline: [
  { id: "revenue", category: "flow" }, { id: "cogs", category: "flow" }, { id: "rnd", category: "opex" },
  { id: "sga", category: "opex" }, { id: "gross_profit", category: "flow" }, { id: "opex", category: "flow" }, { id: "ebit", category: "flow" } ] } }
finicast_define_pivot { model: "nvda", table: "income_statement",
  dims: [{ id: "line", from: "lines" }, { id: "period", from: { periods: { start: "2024-01", count: 6, grain: "year", histUntil: "2026-12-31" } } }],
  lineDim: "line", timeDim: "period" }
finicast_load_table { model: "nvda", table: "financials", source: { csv: "account,period,amount\nrevenue,fy2024,60922\ncogs,fy2024,16621\n..." } }
finicast_define_pivot { model: "nvda", table: "assumptions", dims: [{ id: "driver", from: { distinctOf: "drivers.id" } }, { id: "period", from: "periods" }] }
finicast_set_values { model: "nvda", table: "assumptions", values: [{ at: { driver: "revenue_growth", period: "fy2027" }, value: 0.4 }, ...] }
finicast_set_rules { model: "nvda", table: "income_statement", rules: `
  revenue[frame=hist] = SUM(financials.amount[account=revenue])
  revenue[frame=fcst] = PREV(revenue) * (1 + assumptions.revenue_growth)
  cogs[frame=fcst]    = revenue * assumptions.cogs_pct
  gross_profit        = revenue - cogs
  opex                = SUM(value[line.category = opex])
  ebit                = gross_profit - opex` }
finicast_query { model: "nvda", table: "income_statement", rows: ["line"], cols: ["period"] }

| line         | FY2024 |  FY2025 |  FY2026 | FY2027E | FY2028E | FY2029E |
|--------------|-------:|--------:|--------:|--------:|--------:|--------:|
| Revenue      | 60,922 | 130,500 | 180,000 | 252,000 | 352,800 | 493,920 |
| COGS         | 16,621 |  32,639 |  45,000 |  63,000 |  88,200 | 123,480 |
| Gross Profit | 44,301 |  97,861 | 135,000 | 189,000 | 264,600 | 370,440 |

Example 2 — territory scores from 100,000 activities, grouped through references

finicast_load_table { model: "salesops", table: "territories", source: { inline: [{ id: "t0", name: "West" }, ...] } }
finicast_load_table { model: "salesops", table: "reps", source: { csv: "id,territory\nrep0,t0\n..." } }          // territory detected as a ref
finicast_load_table { model: "salesops", table: "activity_types", source: { inline: [{ id: "call", score: 1 }, { id: "demo", score: 8 }] } }
finicast_load_table { model: "salesops", table: "activities", source: { csv: "id,rep,activity_type,period\n..." },
  options: { computed: [{ id: "score", type: "number" }] } }
finicast_set_rules { model: "salesops", table: "activities", rules: "score = activity_type.score" }
finicast_define_pivot { model: "salesops", table: "territory_scores",
  dims: [{ id: "territory", from: "territories" }, { id: "period", from: { periods: { start: "2026-01", count: 12, grain: "month" } } }], measures: [{ id: "points" }] }
finicast_set_rules { model: "salesops", table: "territory_scores", rules: "points = SUM(activities.score)" }   // grouped through activities.rep → reps.territory
finicast_query { model: "salesops", table: "territory_scores", rows: ["territory"], cols: ["period"] }

Example 3 — an override and an explanation

finicast_set_values { model: "nvda", table: "income_statement", values: [{ at: { line: "revenue", period: "fy2027" }, value: 1000 }] }
finicast_explain    { model: "nvda", table: "income_statement", at: { line: "revenue", period: "fy2028" } }
→ { value: 1400, source: "rule", rule: { text: "revenue[frame=fcst] = PREV(revenue) * (1 + assumptions.revenue_growth)", order: 4 },
    precedents: { revenue: 1000 }, counters: { cellsEvaluated: 2, rowsScanned: 0 } }
finicast_set_values { ..., values: [{ at: { line: "revenue", period: "fy2027" }, value: null }] }   // clear the override

REST

The same operations over HTTP (finidb serve, or /api/db on this host):GET /db/:db/schema · POST /db/:db/tables · POST /db/:db/tables/:table/load (CSV body; into an existing table ?mode=append|upsert|replace, ?dryRun=1 to plan first, ?addFields=1 for new columns; columns match fields by id or name) ·PUT|POST /db/:db/tables/:table/rules · POST /db/:db/cells · POST /db/:db/query (format: markdown|json) ·POST /db/:db/batch · GET /db/:db/changes?since= (long-poll). Errors are { error: { code, message, fix? } } with a 4xx status.

Errors

CodeMeaning and fix
PARSERule text does not parse. One rule per line, target[condition] = expression; quote names with spaces in single quotes.
UNKNOWN_TARGETLeft side is not a measure, line item or field. Use an id from finicast_schema; add the line item to the line table first.
UNKNOWN_NAMEA name is not a measure, line item, dimension or attribute. Check case; qualify other tables: assumptions.growth.
AMBIGUOUS_ATTRIBUTEA bare attribute exists on two dims. Qualify it: period.idx.
AMBIGUOUS_GROUP_KEYTwo reference paths reach one dim. Apply the fix: SUM(deals.acv[owner=@rep]).
SET_IN_SCALARA reference denotes many cells where one is needed. Wrap in an aggregate or pin every dim.
UNPINNED_DIMA cross-pivot reference leaves a dim unshared. Pin it: comm_calc.value[tranche=total].
NO_CURRENT / UNKNOWN_DIM / NO_MEMBERA selector names a dim or member that does not exist here. Use ids from finicast_schema.
BAD_PATH / BAD_REF / BAD_SELECTOR / BAD_CONDITION / BAD_ARGIllegal form. Dots follow refs only; selectors go on values; conditions take members, sets and attribute tests.
NO_TIME_DIMPREV/NEXT/CUMSUM/TRAILING on a pivot without timeDim. Set timeDim or write x[period-1].
NO_PERIODS / UNKNOWN_FUNCTIONPERIOD(date, periods) needs the periods table; SELECT/VLOOKUP do not exist — use references.
RULE_INVALIDNon-strict mode stored the rule but marked it invalid. Fix and resend.
DIV0 / TYPE / NUM / REF / OP / CYCLE / ITERRuntime cell errors, shown as #CODE. Guard with IFERROR/IF; COUNTD for text ids; check referenced rows; a same-period cycle needs iterate: true (or PREV); ITER means it did not converge.
SCHEMA_NO_MODEL / SCHEMA_NO_TABLE / SCHEMA_NO_FIELD / SCHEMA_NO_MEASUREId does not exist. finicast_schema, then use exact ids (slugged: "Cost Center" → cost_center).
SCHEMA_DUPLICATE_*Id already used. Pick another or reuse the existing object (from: "periods").
SCHEMA_REF_NOT_TABULARA ref targets a pivot. References point at tables.
DATA_DUPLICATE_ID / DATA_UNKNOWN_REF / DATA_NO_ROWRow ids must be unique; ref values must be ids of the target table; load dimensions first.
COORD_MISSING_DIM / COORD_NO_MEMBER / QUERY_UNPINNED_DIMGive every dim in at; pin dims not on rows/cols with pages.
NOT_A_PIVOTfinicast_query reads pivots; read table rows with finicast_explain { at: { id } }.

Rules of thumb