API Reference
Hosted MCP server at https://mcp.digitalcalculator.info/mcp (Streamable HTTP, POST, no auth required — optional Bearer tier) · contract 2.10.0 · 10 anonymous tools — 5 scenario tools, 4 single-quantity calculator tools, and money_flow_map — plus prompts, reference-data resources, and per-response provenance. Hosted-API terms: see LICENSE-API.md (acceptable use, rate limits, no warranty). One additional tool, generate_report, is available to Bearer-tier sessions only. Read serverInfo.version from the initialize handshake for the authoritative live number — see the changelog.
What changed at contract 1.0.0 (historical)
Contract 1.0.0 (2026-08-07) consolidated the former granular surface into four scenario tools (plan_retirement_income, evaluate_roth_conversion, check_contribution_eligibility, project_growth), kept the four highest-traffic single-quantity calculators unchanged in purpose, and added prompts, statutory reference-data resources, and the provenance envelope fields (rule_year, sources[]). Every retired granular tool remains reachable indefinitely via its legacy REST alias with its original wire vocabulary. See the changelog for the release entry and the callout below for what changed since.
What changed at 1.2.0
Argument names on the four single-quantity calculators are now snake_case primary — the older camelCase spellings still work but are marked deprecated: true in tools/list and flagged with a namespaced _meta marker on the result; see the deprecation notice. Every JSON-RPC result — success or tool-level error — now carries resultType: "complete" at the result level (never inside structuredContent). Unknown top-level arguments are now rejected with INPUT_VALIDATION on every tool that declares additionalProperties: false (all ten). The anonymous per-IP window now meters tools/call only — discovery methods never count against it. Bearer-tier authentication and rate-limit denials on /mcp are now JSON-RPC error objects (-32001 / -32000) instead of a bare {"error":...} body. plan_retirement_income now returns non-empty provenance[] records. See Envelope shapes, Tool naming convention, and the changelog.
What changed at 1.3.0
INPUT_VALIDATION messages now name their failure class instead of collapsing everything into a range message: a missing field reads "<field> is required", a wrong type reads "<field> must be a number/an integer/a string/a boolean", and an unknown argument reads "unknown argument '<key>' — did you mean '<closest>'? <tool> accepts only: …" (or "— no close match.") — see Unknown arguments are rejected and the worked examples in the Quickstart. Genuine out-of-range messages are unchanged. The 23 input properties whose description already stated a default now carry that default as a real JSON Schema default keyword too, so a client reading the schema mechanically (not just the prose) can see it — canonical spellings only, deprecated camelCase twins do not repeat it. Internal history (finding IDs, "absorbs the former …" clauses, engine file names) was stripped from every description on the anonymous surface. The initialize/server/discover instructions field was restructured — unit contract first, then routing, then provenance and resources, line-broken instead of one 2,073-character paragraph. The retirement-readiness-review prompt now declares annual_return_percent (optional, stated fallback 7) so a host that collects exactly the prompt's declared arguments can complete the review. Every change is additive (MINOR): no previously-accepted request is rejected and no figure moves. See the changelog.
What changed at 1.4.0
One new tool, evaluate_affordability — the fifth scenario tool, listed after the four composites: the maximum home price a budget supports at lender-standard 28/36 debt-to-income targets (caller-adjustable), solved by inverting the same principal-&-interest formula mortgage_monthly_payment uses. It has a REST twin at /v1/tools/evaluate-affordability/calculate and an entry in dc://methodologies/manifest (tool_count 9 → 10); instructions now routes affordability questions to it. Nothing about the nine existing tools changed — no schema, no figure, no message.
What changed at 1.5.0
Two additive capabilities, no new tool and no changed figure. Target mode gives plan_retirement_income an optional target input: state the outcome you want and the tool solves for the input that reaches it, instead of you guessing an input and re-calling. Elicitation lets a call that is missing a required input come back as resultType: "input_required" naming exactly what it needs — the protocol’s 2026-07-28 Multi Round-Trip Request pattern — instead of only failing validation. Both are opt-in: target mode when you send a target block, elicitation when the call declares client support. A client that does neither sees 1.4.0 behaviour byte-for-byte, and the REST twins are unchanged.
What changed at 1.6.0
Reference data became versioned and diffable, with no tool, schema or figure change. Every statutory dc:// resource now carries a version and a content_hash you can recompute, supersedes links the prior rule year where the server holds it, and a new dc://…/diff/{from}..{to} resource lists every field that changed between two rule years — ready for the 2027 figures. Two resources are new: dc://ssa/bend-points/2025 and dc://ssa/bend-points/diff/2025..2026. See Versions, hashes and rule-year diffs.
What changed at 1.7.0
Four additive changes, no new tool and no changed figure. https source links: every success envelope gains source_links[], pairing each dc:// URI in sources[] with the public page that shows the same figures, and each provenance[] record gains the same url beside its uri — a chat host cannot follow a dc:// link. A short reader summary in content: a client that sends the MCP-Protocol-Version header gets the key figures, notes, assumptions, sources and disclaimer as text in content[0], with every figure rounded for reading, while structuredContent keeps the whole result, including year-by-year series; a request without the header keeps the serialized envelope. evaluate_affordability now cites its own methodology page. Tool descriptions and prompt text that named a camelCase argument spelling now name the advertised snake_case one. Existing fields keep their names and values.
What changed at 1.8.0
One naming convention for results, added beside the old one: the v2 envelope. Every camelCase output key gains a snake_case twin, every rate or ratio gains a whole-number *_percent field, notes gain twins that name arguments as the schema does, and every success envelope gains an inputs echo (send it back to reproduce the result) and defaulted_inputs. No key, value or accepted argument changed. The camelCase names are scheduled for removal by rule.
What changed at 2.10.0
Two changes to request handling and one rounding correction; no tool input or output field changes. Anonymous calls that arrive from Anthropic’s published outbound address range, which Claude uses for connector calls, now share one allowance of their own (150 calls per 5 minutes, 5,000 per UTC day) instead of the per-address 30 and 500. And a request that carries the protocol revision 2026-07-28 per-request envelope now gets that revision’s request-metadata checks. A request without the envelope, including those from clients that connect with initialize, is answered exactly as before. And an amount that falls exactly on half a cent now rounds up (away from zero) in every tool, and long daily-compounding projections at very low rates no longer lose digits, so a small share of results move by one cent.
What changed at 2.9.0
Request handling is stricter; no tool input, output field or figure changes. A request carrying an Origin header other than https://www.digitalcalculator.info gets HTTP 403 on every route, as the MCP Streamable HTTP specification requires to prevent DNS rebinding, and a request body not declared as Content-Type: application/json gets HTTP 415. MCP hosts, SDKs, server-side code and curl send no Origin and already declare JSON, so they are unaffected. A free key now comes with a private claimToken, which claiming a dc_pub_ key requires.
What changed at 2.8.0
One estimate becomes more exact; nothing is removed or renamed and no input changes. The social_security module of plan_retirement_income (and the legacy social-security REST alias) now indexes the modeled earnings history to national wage growth, as SSA does: each year of a career from age 22 to the year before full retirement age is capped at that year’s wage base and indexed to the year the worker turns 60, and the highest 35 years count. Figures move only where a salary reaches a year’s cap, almost always down; a worker born in 1959 who earned the taxable maximum every year now gets $4,152.40 at full retirement age, SSA’s published $4,152 (it was $4,234.40). The tool’s engineVersion moves from 2.0.0 to 3.0.0. dc://ssa/bend-points/{year} adds awi_by_year, awi_published_through and awi_scope, and its wage-base table now starts in 1951.
What changed at 2.7.0
One figure changes meaning; nothing is removed or renamed. evaluate_roth_conversion’s break_even_years (and breakEvenYears) is now the first whole year, within the new optional horizon_years (default 20), in which converting is at least even with not converting, compared like with like; otherwise null. At constant rates that is 0 when the retirement rate is at or above the conversion’s effective rate, and null when it is below. It replaces a log-ratio estimate that compared the retirement rate with the marginal rate and could report a break-even of a few years where converting never caught up. The Roth conversion calculator uses the same rule. break_even_note now states the outcome and the gap after the horizon in plain words, and the tool’s engineVersion moves from 1.2.0 to 2.0.0.
What changed at 2.6.0
One additive output; no figure moves and nothing is removed or renamed. plan_retirement_income called with both accumulation and rmd, for someone whose required minimum distributions have not started, adds modules.rmd.first_rmd_estimate: an estimate of the first RMD, with its method in words. See the first-RMD estimate. The tool’s engineVersion moves from 1.0.0 to 1.1.0.
What changed at 2.5.0
One additive notice; no figure moves and nothing is removed or renamed. An argument that is answered although it is outside its documented range adds a warnings[] entry with code: "input_out_of_range", naming the argument, the value it received and the accepted range. Today one argument reaches it: paycheck_net_pay’s legacy dependents, documented as a whole number from 0 to 20, which answered values such as -1, 2.5 and 21 without saying so. Out-of-range values for every other documented range are already refused with INPUT_VALIDATION. Warned values will be refused too, no earlier than 2027-05-27.
What changed at 2.4.0
One additive notice; no figure moves and nothing is removed or renamed. check_contribution_eligibility and plan_retirement_income (with the accumulation module) add a warnings[] entry with field: "rule_year" when the IRS figures behind the answer may be out of date: rule_year_newer_published when the IRS has published a newer year’s figures that this server does not apply yet (today, the 2027 HSA limits on kind: "hsa" answers), and rule_year_past_review once the rule year’s figures pass their review date. The entry is absent when there is nothing to say.
What changed at 2.3.0
Two new optional arguments and two new output fields on paycheck_net_pay; nothing removed or renamed. children_under_17 and other_dependents apply Form W-4 (2026) Step 3’s two amounts ($2,200 per qualifying child under 17, $500 per other dependent) instead of the flat $2,000-per-dependents reading, and only when total income is at or below the form’s $200,000 / $400,000 (married filing jointly) limit; dependent_credit_status says whether the credit applied, was never claimed, or was blocked by the income limit. The legacy dependents argument still works, read as qualifying children under 17; sending it alongside either new argument is refused. When dependents is what moved the credit, dependents_reading_note discloses the reading; null otherwise.
What changed at 2.2.0
One correction and one new optional argument; nothing removed or renamed. plan_retirement_income’s rmd module reported a start age of 70½ for everyone born in 1950 or earlier; people born from July 1, 1949 through 1950 start at 72 under the SECURE Act of 2019, and the module now says so, with the matching first RMD year. The optional birth_month decides 1949 (before July: 70½; from July: 72). Without it, a 1949-or-earlier birth is read as the first half of the year, the earlier answer, and assumptions.birth_month discloses it. RMD amounts do not change.
What changed at 2.1.0
Two corrections and one new optional argument; nothing removed or renamed. project_growth takes rate_type: leave it out (or pass nominal) and nothing changes; pass apy for a bank’s advertised APY and the figures match the savings calculator. plan_retirement_income’s accumulation module now applies the IRC 415(c) limit on employee plus employer additions, as the 401(k) calculator does, and adds annual_additions_note when it binds; only high-salary, large-match projections move, and they move down. The emergency_fund_recommendation description now says what is true: the tool and the site’s calculator run one engine. The interactive views follow: the mortgage view reads the snake_case argument names, and the 401(k) view applies the same limit.
What changed at 2.0.0 (MAJOR)
paycheck_net_pay no longer computes state income tax for any state, including the 9 states with no tax on wage income. Unless you pass the new optional argument state_local_tax_annual (your own annual state and local tax, used exactly), state_tax, state_tax_annual, state_effective_rate_percent, state_tax_source and no_state_income_tax are null, the new state_tax_status field reads not_provided, state_tax_notes says state and local taxes are not included, and net pay is after federal tax and FICA only. state is still accepted and validated, and changes no figure. Nothing was removed or renamed, but fields that were always numbers or booleans can now be null, so this is a MAJOR release; the 90-day notice was waived and disclosed (see the contract). Net pay for a state that taxes wages is higher than before because the old estimate is no longer deducted.
What changed at 1.11.0
Two more interactive views for MCP Apps hosts, and nothing a tool returns differently. emergency_fund_recommendation now links a ui:// view (a progress ring and a month-by-month savings line), and the project_growth view is rebuilt as a stacked area of the money put in and the growth on it. Both are not yet served by the hosted endpoint. recompute_scenario now also accepts emergency_fund_recommendation. The server also counts calls per tool per day, recording the tool name and the date only. The first views of this kind arrived at 1.10.0 (one home-scenario view for mortgage_monthly_payment and evaluate_affordability, and the rebuilt loan view), on the foundation 1.9.0 laid: the app-only recompute_scenario tool and content-hashed view URIs.
Calling the server
The server speaks the MCP Streamable HTTP transport: JSON-RPC 2.0 over HTTP POST. Supported methods: initialize, server/discover, ping, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get. A tool invocation looks like:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "mortgage_monthly_payment",
"arguments": { "principal": 300000, "annual_rate_percent": 6.5, "term_years": 30 }
}
}Successful calls return a JSON-RPC envelope whose result.structuredContent carries the ToolResult object described below, with result.resultType: "complete" as a sibling field (see Envelope shapes). result.content[0].text carries the same ToolResult serialized as JSON when the request has no MCP-Protocol-Version header, and a short plain-text summary of it when the header names 2025-06-18 or later — see The content summary; read structuredContent for the data either way. Tool failures set result.isError: true with the ToolError JSON in the same content slot — resultType is still present at the result level, since this is a successful JSON-RPC call that reports a tool-level failure. A malformed request or unknown method is a different thing entirely: a JSON-RPC error object, which carries no resultType at all (see Status mapping).
Protocol versions (2026-07-28 support)
Supported protocol revisions: 2026-07-28, 2025-11-25, 2025-06-18 (default 2026-07-28). Two handshakes are served from the same stateless code path:
initialize— the classic session-oriented handshake, kept working unchanged for older hosts. Carries the requested version inparams.protocolVersion.server/discover— the 2026-07-28 stateless discovery handshake: noinitializednotification expected, no session header issued or required. The requested version may also arrive viaparams._meta.protocolVersionon any request (the session-header-free carrier).
Because this server is deterministic and its discovery surfaces change only on deploy, static responses (server/discover, tools/list, resources/list, prompts/list, and every resources/read body) advertise their own cacheability: a ttlMs of 24 hours (86,400,000ms) with cacheScope: "public" ("private" on the authenticated tools/list, which additionally lists generate_report). As of contract 1.2.0 the four list-shaped results (server/discover, tools/list, resources/list, prompts/list) carry ttlMs / cacheScope as top-level result fields; resources/read is not a list result, so it carries the same two facts namespaced at _meta["info.digitalcalculator/cache"] instead — see Cacheability advertisements. Clients that honor the advertisement can skip repeat discovery round-trips entirely.
Request metadata (contract 2.10.0)
A request that carries the 2026-07-28 per-request envelope — params._meta with io.modelcontextprotocol/protocolVersion (a string) and io.modelcontextprotocol/clientCapabilities (an object), and optionally io.modelcontextprotocol/clientInfo — is checked against its HTTP headers, as that revision’s Streamable HTTP transport requires. It must send MCP-Protocol-Version equal to the envelope’s version and Mcp-Method equal to the body’s method; tools/call and prompts/get also send Mcp-Name equal to params.name, and resources/read sends it equal to params.uri (plain, or as a =?base64?…?= value). Each answer below is a JSON-RPC error that echoes the request id:
- HTTP 400,
-32602— the envelope is malformed;error.data.envelopenames the key and the problem - HTTP 400,
-32020— a required header is missing or disagrees with the body;error.data.mismatchgives the header value and what the body said - HTTP 400,
-32022— the envelope names a revision from 2026-07-28 on that this server does not serve;error.datalistssupportedand echoesrequested - HTTP 404,
-32601— the method is unknown
A request without the envelope key is not checked and is answered as before, even when it sends MCP-Protocol-Version: 2026-07-28; an unknown method there stays HTTP 200 with -32601. initialize and ping are still answered under the envelope.
REST companion
A REST API at https://api.digitalcalculator.info/v1/tools/{alias}/calculate runs the exact same engines and returns the same response envelope without the JSON-RPC wrapper. The four scenario tools have kebab-case aliases speaking the canonical vocabulary (plan-retirement-income, evaluate-roth-conversion, check-contribution-eligibility, project-growth), and every alias that predates the contract-1.0.0 consolidation keeps working — see Legacy REST aliases. (The earlier single-endpoint mortgage POC at /api/calculate/mortgage is legacy — do not build new integrations against it.)
Envelope shapes
JSON-RPC Result envelope
Every tools/call response — success or tool-level error — is a JSON-RPC Result (not a JSON-RPC error). resultType sits directly on the result object, a sibling of content and structuredContent — never nested inside structuredContent, which mirrors the REST response body byte-for-byte and is what a compliant client validates against the tool's outputSchema:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "Mortgage payment (principal and interest only):\n- Monthly payment: $1,896.20\n…" }
],
"structuredContent": {
"result": {
"monthlyPayment": 1896.20, "monthly_payment": 1896.20,
"totalInterest": 382633.47, "total_interest": 382633.47,
"totalPaid": 682633.47, "total_paid": 682633.47
},
"disclaimer": "…", "methodology": { "url": "…", "version": "…" },
"rule_year": 2026, "sources": ["dc://methodologies/manifest"],
"source_links": [{ "uri": "dc://methodologies/manifest", "url": "https://www.digitalcalculator.info/mortgage-calculator/methodology/" }],
"provenance": [],
"calculatedAt": "…", "calculated_at": "…",
"engineVersion": "1.0.1", "engine_version": "1.0.1",
"inputs": { "principal": 300000, "annual_rate_percent": 6.5, "term_years": 30 },
"defaulted_inputs": []
},
"resultType": "complete"
}
}The pinned value set for resultType is "complete" | "input_required". Every complete answer — success or tool-level error — is "complete"; since contract 1.5.0 a call that declares elicitation support and is missing a required input gets "input_required" instead. A JSON-RPC error object (malformed request, unknown method, the Bearer/throttle denials below) is a different response shape and carries no resultType at all — see Status mapping.
ToolResult<T> (success)
The object at result.structuredContent (also serialized as JSON in result.content[0].text for requests without the MCP-Protocol-Version header):
interface ToolResult<T> {
result: T; // Tool-specific structured object
disclaimer: string; // Canonical YMYL disclaimer (dc://disclaimers/ymyl)
methodology: {
url: string; // Absolute URL to the primary methodology page
version: string; // ISO date of last data verification
};
rule_year: number; // Statutory year the computation used (provenance)
sources: string[]; // dc:// resource URIs the computation drew on
source_links: Array<{ // 1.7.0: the https twin of each sources[] entry
uri: string; // The dc:// resource (also listed in sources[])
url: string; // Public page showing the same figures
}>;
provenance: Array<{ // Resolved provenance for those resources
uri: string; // The dc:// resource (also listed in sources[])
url: string; // 1.7.0: the https twin of uri (same as source_links)
table?: string; // Section of a multi-table resource, when applicable
source: string; // Citation of the primary document
source_url: string; // https link to the primary document
last_verified: string; // YYYY-MM-DD the tables were verified against it
}>;
calculatedAt: string; // ISO-8601 UTC timestamp (deprecated since 1.8.0)
calculated_at: string; // 1.8.0: the same timestamp
engineVersion: string; // SemVer of engine that produced result (deprecated since 1.8.0)
engine_version: string; // 1.8.0: the same SemVer
inputs: Record<string, unknown>; // 1.8.0: the arguments used, snake_case, defaults included
defaulted_inputs: string[]; // 1.8.0: the inputs entries that came from a default
}Since contract 1.8.0 every camelCase key, in the envelope and in result, also has a snake_case twin beside it; see The v2 envelope.
Provenance fields (added at contract 1.0.0; extended at 1.2.0)
Every success envelope carries rule_year — the statutory year the computation used (currently 2026, the only supported rule year; passing any other value is rejected rather than silently computed against the wrong tables) — and sources[], the dc:// reference-data resource URIs the computation drew on (IRS limits, the Uniform Lifetime Table, SSA bend points, the methodology manifest). Scenario tools cite multiple sources; each URI is readable via resources/read. The provenance block is what a compliance reviewer audits — it is why an agent can put these numbers in front of a client with a citation trail.
The envelope also carries provenance[], which answers “where did this figure come from, and when did you last check it?” without a second round trip: for each statutory resource the computation drew on, the citation of the primary document, an https link to it, and the date our mirrored tables were last verified against it. Where a resource holds several tables (dc://irs/limits/{year} holds the 401(k), IRA, HSA and federal-tax tables), the record names the table the call actually used — an HSA answer cites Rev. Proc. 2025-19, not the 401(k) notice. Every field is read from the reference-resource payload itself, so a response cannot cite a date that disagrees with what resources/read serves. Pure-math tools (mortgage, loan, growth) draw on no statutory table and carry provenance: []; the field is always present. As of contract 1.2.0, plan_retirement_income carries a non-empty provenance[] record per statutory resource actually used by the modules you selected in include[] (before 1.2.0 it echoed [] even for a statutory answer); evaluate_roth_conversion, check_contribution_eligibility and paycheck_net_pay already carried it.
Each cited source is also emitted as an MCP resource_link content block, appended after the text block in the content[] array of every successful tools/call. So provenance is not just an asserted string — a host can fetch the citation directly. The sources[] string array inside structuredContent is unchanged and remains the canonical field; the links are an additional rendering of the same facts, and a client that ignores resource_link sees exactly the response it saw before.
https source links (source_links, contract 1.7.0)
A dc:// URI is readable through resources/read, but a person reading an answer in a chat client cannot follow it. Every success envelope therefore also carries source_links[]: one { uri, url } pair per sources[] entry, in the same order, where url is the public page on this site that shows the same figures. Each provenance[] record carries the same url beside its uri. sources[] itself is unchanged.
| Resource | Page |
|---|---|
dc://irs/limits/2026, 401(k) table | /401k-retirement-calculator/methodology/ |
dc://irs/limits/2026, IRA table | /ira-calculator/methodology/ |
dc://irs/limits/2026, HSA table | /hsa-calculator/methodology/ |
dc://irs/limits/2026, federal tax table | /paycheck-calculator/2026-tax-brackets/ (brackets and standard deductions; the FICA rates are on the paycheck methodology page) |
dc://irs/uniform-lifetime | /rmd-calculator/methodology/ |
dc://ssa/bend-points/2026 | /social-security-calculator/methodology/ |
dc://methodologies/manifest | The calling tool’s own methodology page (its entry in the manifest) |
Where no page shows a resource, the link is the tool’s methodology page — the same URL as methodology.url, which is that tool’s entry in dc://methodologies/manifest. That applies to the methodology manifest itself, to dc://irs/limits/{year} when a call names no single table, and to any rule year the site’s pages do not publish: a page showing 2026 figures is never offered as the twin of another year’s table.
The content summary (contract 1.7.0)
Before 1.7.0, content[0].text repeated the whole ToolResult as JSON, so a model read every field twice — for plan_retirement_income that included the full year-by-year series. Since 1.7.0 a client that sends the MCP-Protocol-Version header with 2025-06-18 or later (the revisions that define structuredContent) gets a short plain-text summary in content[0].text instead: the key figures, any notes, the assumptions the server applied, any unit warnings, the methodology link, the sources with their https pages, and the canonical disclaimer, followed by a line saying the complete result is in structuredContent. Currency is shown to the cent and percentages and ratios to at most two decimals; a value you entered is quoted as entered. Where an older engine note names an argument in camelCase (for example workplaceCoverage), the summary spells it the way the tool’s schema does (workplace_coverage); the note in structuredContent keeps its existing text.
structuredContentis the same in both cases — nothing is removed from it, and theresource_linkblocks still follow the text block.- A request without the header — for example a plain HTTP POST — keeps the serialized ToolResult JSON in
content[0].text, exactly as before 1.7.0. - Either format can be requested per call with
params._meta["info.digitalcalculator/contentFormat"]:"summary"or"json". - Tool-level errors are never summarized:
content[0].textstays the ToolError JSON.
Applied-default disclosure (assumptions)
plan_retirement_income and project_growth accept optional inputs that carry a default — employer match, salary growth, catch-up eligibility, years worked, life expectancy, annual contribution, compounding frequency. When you omit one, the applied default is echoed in an assumptions object on the result, keyed by field name:
"assumptions": {
"annual_salary_growth_percent": {
"value": 0,
"defaulted": true,
"note": "No salary growth was provided, so salary was held flat for the whole projection. A real raise history would raise the projected balance."
}
}An empty object means every value used was supplied by the caller — the block is always present, so a client never has to branch on its existence. For plan_retirement_income the disclosure is scoped to the modules you actually selected: an RMD-only call assumes nothing and returns {}. Present these to an end user as assumptions rather than as their own figures.
Scope, stated plainly: this reports defaults the server applied. It cannot detect a rate an agent invented and passed in — annual_return_percent: 7 arrives indistinguishable from a number the user stated. Required rate inputs (annual_return_percent, annual_rate_percent) have no default for exactly that reason: they must come from the user, or be proposed to them explicitly first.
The v2 envelope (contract 1.8.0)
Before 1.8.0 the results did not share one convention: the older tools answered in camelCase (monthlyPayment) and the newer ones in snake_case (future_value); every rate goes in as a whole-number percent, but some came out as decimals (marginalRateOnConversion: 0.22, stateEffectiveRate: 0.043909); and only some tools echoed their inputs. Since 1.8.0 every success envelope of the ten anonymous tools — on tools/call and on each tool’s REST twin — also carries one consistent form, beside the keys it already had. Nothing was renamed or removed: every existing key keeps its value.
- A snake_case twin of every camelCase key, placed right after it, at every depth. Under a camelCase key the twin holds the same value with every key beneath it in snake_case (
modules.accumulation.year_by_year[]rows reademployee_contribution,ending_balance); under a key that is already snake_case the twins are added in place (modules.accumulationgainsfuture_balancebesidefutureBalance). - A
*_percentfield for every rate or ratio, as a whole-number percent (the table below). A decimal ratio is multiplied by 100 and rounded to 2 decimals; a computed percent is rounded to 2 decimals; a percent that echoes your own argument keeps your exact value (6.125 stays 6.125). - Notes that name arguments the way the schema does. A few engine notes predate the snake_case argument names; their twins say
workplace_coverageandcatch_up_enabled, and name output keys by their v2 names. (The Roth break-even note did the same forretirement_marginal_rate_percentuntil 2.7.0 rewrote it in plain words.) Everywarnings[]entry gainsargument, the advertised name of the argument itsfieldnames. calculated_atandengine_version, the twins of the envelope’s own camelCase keys.inputs: the arguments the result was computed from, by their snake_case names — what you sent (a deprecated camelCase spelling is reported under its replacement) plus every default the tool applied. An optional argument with no default that you omitted is absent. Sendinputsback as the arguments and you get the same result.defaulted_inputs: the names of theinputsentries that came from a default rather than from you. It agrees withassumptionswhere a tool has one (every field there is listed here), and it also covers the tools that have none — for examplerule_yearandexpected_return_percentbelow.
{
"result": {
"rule_year": 2026,
"taxOwed": 6600,
"tax_owed": 6600,
"marginalRateOnConversion": 0.22,
"marginal_rate_on_conversion_percent": 22,
"effectiveRateOnConversion": 0.22,
"effective_rate_on_conversion_percent": 22,
"fiveYearRuleApplies": true,
"five_year_rule_applies": true,
"breakEvenYears": null,
"break_even_years": null,
"breakEvenNote": "Break-even needs your expected retirement marginal rate; …",
"break_even_note": "Break-even needs your expected retirement marginal rate; …",
"meta": { "tableYear": 2026, "table_year": 2026, "source": "IRS Rev. Proc. 2025-32" }
},
…,
"calculatedAt": "…", "calculated_at": "…",
"engineVersion": "2.0.0", "engine_version": "2.0.0",
"inputs": {
"conversion_amount": 30000, "birth_year": 1970, "filing_status": "married_filing_jointly",
"current_taxable_income": 120000, "expected_return_percent": 7, "horizon_years": 20, "rule_year": 2026
},
"defaulted_inputs": ["expected_return_percent", "horizon_years", "rule_year"]
}| Deprecated key | v2 key | Value |
|---|---|---|
monthlyRate (loan) | monthly_rate_percent | decimal × 100, rounded to 2 decimals |
marginalRateOnConversion (Roth) | marginal_rate_on_conversion_percent | decimal × 100, rounded to 2 decimals |
effectiveRateOnConversion (Roth) | effective_rate_on_conversion_percent | decimal × 100, rounded to 2 decimals |
stateEffectiveRate (paycheck) | state_effective_rate_percent | decimal × 100, rounded to 2 decimals |
savingsRatePercent (money flow) | savings_rate_percent | rounded to 2 decimals |
percentOfIncome (money flow categories) | income_share_percent | rounded to 2 decimals |
percentComplete (emergency fund) | funded_percent | rounded to 2 decimals |
annualRatePercent (loan echo) | annual_rate_percent | your argument, exactly |
savingsAPY (emergency fund echo) | savings_apy | your argument, exactly |
Every other deprecated key’s twin is its snake_case spelling, acronyms lowercased: adjustedMonthlyBenefit → adjusted_monthly_benefit, annualAdditionsNote → annual_additions_note, bendPointsEstimated → bend_points_estimated, breakEvenNote → break_even_note, breakEvenYears → break_even_years, calculatedAt → calculated_at, catchUp → catch_up, catchUpAmount → catch_up_amount, catchUpEligible → catch_up_eligible, catchUpNote → catch_up_note, colaAdjustment → cola_adjustment, contributionLimit → contribution_limit, currentSavings → current_savings, dependentCreditAnnual → dependent_credit_annual, distributionPeriod → distribution_period, eligibilityYear → eligibility_year, eligibleContribution → eligible_contribution, employeeContribution → employee_contribution, employeeMaxRemaining → employee_max_remaining, employerContribTotal → employer_contrib_total, employerContribution → employer_contribution, employerContributionExceedsLimit → employer_contribution_exceeds_limit, employerMatch → employer_match, employmentType → employment_type, endingBalance → ending_balance, engineVersion → engine_version, excessContribution → excess_contribution, excessContributionNote → excess_contribution_note, federalTax → federal_tax, federalTaxAnnual → federal_tax_annual, federalTaxBeforeDependentCreditAnnual → federal_tax_before_dependent_credit_annual, ficaTax → fica_tax, ficaTaxAnnual → fica_tax_annual, fiveYearRuleApplies → five_year_rule_applies, fraAge → fra_age, fundGap → fund_gap, futureBalance → future_balance, futureValue → future_value, grossPerPaycheck → gross_per_paycheck, growthEarned → growth_earned, incomeEarners → income_earners, interestEarned → interest_earned, jointLifeTableMayApply → joint_life_table_may_apply, largestCategories → largest_categories, latestPublished → latest_published, lifeExpectancyFactor → life_expectancy_factor, lifetimeBenefitProjection → lifetime_benefit_projection, loanAmount → loan_amount, mapUrl → map_url, monthlyBenefitAtFRA → monthly_benefit_at_fra, monthlyExpenses → monthly_expenses, monthlyPayment → monthly_payment, monthlySavings → monthly_savings, monthlyTotal → monthly_total, monthsToGoal → months_to_goal, netPay → net_pay, netPayAnnual → net_pay_annual, noStateIncomeTax → no_state_income_tax, numPayments → num_payments, payPeriodsPerYear → pay_periods_per_year, payrollDeductionTaxSavings → payroll_deduction_tax_savings, penaltyIfMissed → penalty_if_missed, phaseOutReduction → phase_out_reduction, piaAtEligibility → pia_at_eligibility, recommendedMonths → recommended_months, rmdAmount → rmd_amount, rothCap → roth_cap, spouseBeneficiaryNote → spouse_beneficiary_note, stateTax → state_tax, stateTaxAnnual → state_tax_annual, stateTaxNotes → state_tax_notes, stateTaxSource → state_tax_source, superCatchUpApplies → super_catch_up_applies, tableUsed → table_used, tableYear → table_year, targetFund → target_fund, taxOwed → tax_owed, termYears → term_years, totalContributionLimit → total_contribution_limit, totalContributions → total_contributions, totalInterest → total_interest, totalMonthlyIncome → total_monthly_income, totalMonthlySpending → total_monthly_spending, totalPaid → total_paid, traditionalCap → traditional_cap, traditionalDeductionNote → traditional_deduction_note, traditionalDeductionStatus → traditional_deduction_status, yearByYear → year_by_year. Enum values keep their spelling (traditional_deduction_status: "fullyDeductible"). The output schemas declare every twin and mark each camelCase property deprecated: true; result.warnings[].field is deprecated too, in favour of argument.
Why beside, not inside a v2 object. The camelCase names will be removed (2 minor versions or 60 days after production serves 1.8.0, whichever is later). Because the new keys sit next to the old ones, the removal deletes keys and every new key stays at the path it has today, so code written against the new keys keeps working through the removal. Read only the snake_case and *_percent keys in new code.
Size: until the removal each envelope carries both spellings, so structuredContent is roughly 20–60% larger for typical calls; a client that sends MCP-Protocol-Version reads the content summary, which is unchanged. tools/list did not grow: the deprecated properties give their descriptions to their twins.
Monetary precision (round-to-cents)
Every USD-denominated output field is a JSON number rounded to the nearest cent (2 decimal places) — not full floating-point precision. Non-monetary fields (rates, ratios, counts, months, ages, IRS table factors) keep their natural precision.
structuredContent is success-only
result.structuredContent is present ONLY on a successful tools/call (isError absent). A tool-level failure (result.isError: true) carries the ToolError JSON in content[0].text and carries NO structuredContent — check for the object's presence rather than assuming its shape. resultType, by contrast, is present on both the success and the tool-level-error result (see JSON-RPC Result envelope).
Cacheability advertisements (ttlMs / cacheScope)
As of contract 1.2.0, ttlMs and cacheScope live in two different places depending on the result shape. On tools/list, resources/list, prompts/list, and server/discover — every LIST-shaped result — they are top-level fields of the result object, siblings of tools / resources / prompts (e.g. { "tools": [...], "ttlMs": 86400000, "cacheScope": "public" }). resources/read is not a list result, so its cache advertisement lives under a namespaced _meta["info.digitalcalculator/cache"] object instead: { "_meta": { "info.digitalcalculator/cache": { "ttlMs": 86400000, "cacheScope": "public" } } }. Neither field appears on initialize or a tools/call result (per-input, not cacheable). Before 1.2.0 both fields sat inside a bare _meta.ttlMs / _meta.cacheScope on every one of these shapes; no host was known to read that placement.
ToolError (failure)
interface ToolError {
error: {
code: "INPUT_VALIDATION" | "BUSINESS_RULE" | "INTERNAL"
| "RATE_LIMIT" | "UNAUTHORIZED";
message: string; // Human-readable
field?: string; // Input field that failed (validation only)
retriable: boolean; // Only RATE_LIMIT is retriable
};
}Status mapping
- 200 + ToolResult — success
- 200 +
isError: true— tool-level error (INPUT_VALIDATION,BUSINESS_RULE) per the MCP spec. This is the shape a schema-validation failure takes — a rejected argument is never an HTTP 400; it is a normal 200 response whoseresult.isErroristrue - JSON-RPC error object — malformed request, unknown method (protocol-level errors), and (as of contract 1.2.0) the Bearer and rate-limit denials on
/mcpdescribed below - 401 on
/mcp(Bearer, contract 1.2.0) — anAuthorization: Bearercredential that is invalid, revoked, or expired returns HTTP 401 with a JSON-RPC error body:{"jsonrpc":"2.0","id":<recovered-id>,"error":{"code":-32001,"message":"<reason>: not accepted.","data":{"reason":"invalid_token"|"token_revoked"|"token_expired"}}}. NoX-RateLimit-*headers on a 401. Sending noAuthorizationheader is not an error — that is the anonymous tier - 401 on the REST route (
POST /v1/tools/{alias}/calculate) — unchanged:{"error":{"code":"UNAUTHORIZED","message":"...","retriable":false}}, no JSON-RPC wrapper (Amendment B pin #3) - 429 on
/mcp(contract 1.2.0) — both the anonymous per-IP window and a Bearer key's throttle return HTTP 429 with a JSON-RPC error body, code-32000:{"jsonrpc":"2.0","id":<recovered-id>,"error":{"code":-32000,"message":"<reason>: slow down.","data":{"reason":"rate_limited"|"quota_exceeded","scope":"window"|"daily","window":"5 minutes"|"1 day (UTC)","limit":30,"retry_after_seconds":42,"reset_epoch":1800000000}}}. HTTP status and theRetry-After/X-RateLimit-*/X-Quota-*headers are byte-identical to the pre-1.2.0 shape — only the response body moved from a bare object into the JSON-RPC error envelope. Onlytools/callis metered by the anonymous window:initialize,server/discover,ping,tools/list,resources/list,resources/read,prompts/list,prompts/getand theinitializednotification never reach the throttle, so a host's connect sequence can never be rate-limited — a request the server cannot parse (unparseable body, batch request, oversize body, missing/unknown method) is still metered, fail-closed - 429 on the REST route — unchanged:
{"error":{"code":"RATE_LIMIT","message":"...","retriable":true}} - 403 (contract 2.9.0) — a request with an
Originheader other thanhttps://www.digitalcalculator.info, on every route. On/mcp:{"jsonrpc":"2.0","error":{"code":-32600,"message":"Origin not allowed. …","data":{"reason":"origin_not_allowed"}}}, with noid, as the MCP Streamable HTTP specification allows; on the REST routes,INPUT_VALIDATIONin the usual envelope. A request with noOriginheader is not affected - 415 (contract 2.9.0) — a request body not declared as
Content-Type: application/json(parameters such ascharset=utf-8are fine). Same two body shapes as the 403, with"reason":"unsupported_media_type". The bodylessPOST /v1/keys/publicneeds noContent-Type - 400 and 404 on
/mcpfor an enveloped request (contract 2.10.0) — only a request whoseparams._metacarriesio.modelcontextprotocol/protocolVersion; see Request metadata for the codes - 500 —
INTERNAL(shouldn't happen in production; report it if you see one)
REST error payloads are single-envelope {"error":{"code":...}} — never double-nested. The two JSON-RPC error codes above sit in the spec's implementation-defined server-error range and are distinct from each other: -32001 (UNAUTHORIZED) is never returned for a rate limit, and -32000 (RATE_LIMITED) is never returned for a bad credential.
Unknown arguments are rejected (strict schemas)
Every tool's advertised inputSchema.additionalProperties is false, and as of contract 1.2.0 that promise is enforced at tools/call for all ten tools (it was already enforced for money_flow_map; the other nine previously stripped unrecognized keys silently). As of contract 1.3.0, the message also proposes the fix when there's a close match — a typo like term_year gets a “did you mean” naming term_years, while an invented key with no near match, like down_payment, gets “no close match” and the accepted-keys list. Either way the call fails closed with INPUT_VALIDATION, field set to the first unknown key, instead of quietly computing an answer that ignored the input:
{
"error": {
"code": "INPUT_VALIDATION",
"message": "unknown argument 'down_payment' — no close match. mortgage_monthly_payment accepts only: principal, annual_rate_percent, term_years. Unknown keys are rejected, not ignored, so a misspelled argument cannot silently change the answer.",
"field": "down_payment",
"retriable": false
}
}The deprecated camelCase spellings (annualRatePercent, termYears, and the rest of the 17 aliases) are declared properties, never unknown ones — sending one still succeeds and adds the deprecation marker described below. generate_report enforces strictness on its own top level (tool, input); its input object is an open object by design (see the Amendment B exception). The REST route is untouched: POST /v1/tools/{alias}/calculate still accepts an unrecognized key silently, exactly as it always has.
Target mode — state the outcome, not the input (contract 1.5.0)
Every tool on this surface answers the forward question: given these inputs, what is the balance / payment / benefit? plan_retirement_income also answers the inverse one. Send an optional target block naming the outcome you want and which input to solve for, and omit that input; the tool finds the value that reaches the outcome by inverting its own projection with a bracketed goal-seek (the same search evaluate_affordability uses), so the answer and the projection returned beside it can never disagree.
{
"include": ["accumulation"],
"birth_year": 1978,
"balance": 250000,
"annual_salary": 120000,
"contribution_percent": 10,
"annual_return_percent": 6,
"target": { "metric": "future_balance", "value": 1200000, "solve_for": "retirement_age" }
}target.metric is future_balance (projected 401(k) balance at retirement, USD — needs the accumulation module) or monthly_benefit (Social Security monthly benefit at the chosen claim age, USD per month — needs the social_security module). target.value is in that metric's unit. target.solve_for must be an input that both moves the metric and is a dial you can actually turn:
solve_for | Searched range | Reaches |
|---|---|---|
contribution_percent | 0 – 100 | future_balance |
annual_return_percent | 0 – 20 | future_balance |
retirement_age | your rule-year age + 1 – 100 (whole years) | future_balance |
balance | 0 – 100,000,000 | future_balance |
annual_salary | 0 – 100,000,000 (0 – 1,000,000 when social_security is selected) | future_balance, monthly_benefit |
claim_age | 62 – 70 | monthly_benefit |
What the tool will not solve for, and why. birth_year, birth_month and spouse_birth_year are facts, not choices — and birth_year is the shared age basis every module steps on (catch-up cohorts, the SECURE 2.0 RMD start age, full retirement age, bend-point coverage), so solving it would answer “be born later”. years_worked is your earnings record. life_expectancy is an assumption you do not choose. employer_match_percent, employer_match_limit_percent and annual_salary_growth_percent are your employer's terms and your future raises, not dials. catch_up_enabled and is_spouse_sole_beneficiary are booleans with no continuum between them — call the tool once each way. rule_year has one legal value, and include[] is a list. Asking for any of these returns INPUT_VALIDATION quoting the reason, not a bare enum list.
The result gains a target block beside modules. On success it carries solved_value, achieved_value (the metric evaluated at solved_value — the same figure modules reports), difference_from_target, searched_range and a plain-language note; a whole-year retirement_age solve also carries rounded_to and may overshoot, because it reports the first whole year that reaches the target.
An unreachable target is an answer, not an error. The IRS §402(g) elective-deferral cap, for example, bounds what contribution_percent can reach at any salary — past a certain point contributing more changes nothing. When no value in the range reaches the target the call still succeeds, with reached: false, a reason, range_endpoints giving the metric at both ends of the search, and modules_computed_at naming the value the projection below was actually computed at. Nothing is clamped silently and nothing is thrown:
{
"target": {
"metric": "future_balance",
"requested_value": 500000000,
"solve_for": "contribution_percent",
"searched_range": { "minimum": 0, "maximum": 100 },
"reached": false,
"solved_value": null,
"reason": "out-of-range",
"range_endpoints": [
{ "solve_for_value": 0, "metric_value": 673193.20 },
{ "solve_for_value": 100, "metric_value": 1364408.75 }
],
"modules_computed_at": { "contribution_percent": 100 },
"note": "No value of contribution_percent between 0 and 100 reaches … NOT at your target."
}
}A call with no target block behaves exactly as it did at 1.4.0, and no target key appears in its result.
Elicitation — the server asks for a missing input (contract 1.5.0)
A call missing a required input can come back as a question instead of only a validation failure, using the Multi Round-Trip Request pattern of protocol revision 2026-07-28. (That revision removed server-initiated requests; a server can no longer send you an elicitation/create out of band. MRTR rehomes the same capability inside the response to your own call.)
You must declare support on the call. This server keeps no session state — it cannot recall the capabilities you sent at initialize — and the spec forbids sending an inputRequests type a client has not declared. So the declaration travels with the request, in params._meta:
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "plan_retirement_income",
"arguments": { "include": ["rmd"], "birth_year": 1951 },
"_meta": { "info.digitalcalculator/elicitation": true }
}
}Without that key the call is answered exactly as it was at 1.4.0 — an INPUT_VALIDATION ToolError. With it, and only when something required is absent, the result is:
{
"resultType": "input_required",
"inputRequests": {
"missing_inputs": {
"method": "elicitation/create",
"params": {
"message": "plan_retirement_income needs one more value before it can answer: balance (number, USD).",
"requestedSchema": {
"type": "object",
"properties": { "balance": { "title": "Balance", "type": "number", "minimum": 0, "maximum": 100000000, "description": "Retirement account balance in USD. …" } },
"required": ["balance"]
}
}
}
},
"requestState": "eyJ2IjoxLCJ0b29sIjoicGxhbl9yZXRpcmVtZW50X2luY29tZSIsIC4uLn0",
"content": [{ "type": "text", "text": "plan_retirement_income needs one more value …" }]
}Every missing field is named in one request, and requestedSchema is derived from the tool's own published input schema — so each field arrives with its real type, range, enum, default and the description carrying its unit. This is a Result, not a failure: there is no isError, and no structuredContent (that field mirrors the REST body, and REST has no MRTR).
To finish the call, collect the values and retry the original request with a new JSON-RPC id, echoing requestState and adding inputResponses keyed by the same server-assigned key:
"params": {
"name": "plan_retirement_income",
"arguments": { "include": ["rmd"], "birth_year": 1951 },
"_meta": { "info.digitalcalculator/elicitation": true },
"requestState": "eyJ2IjoxLCJ0b29sIjoi…",
"inputResponses": { "missing_inputs": { "action": "accept", "content": { "balance": 500000 } } }
}Answer with action: "decline" or "cancel" and the server stops asking and returns the ordinary validation error — as it also does after three rounds, so a client that cannot supply the field can never be held in a loop. Only properties the tool actually declares are accepted from content, so strict schemas hold through this door too. The server asks only for what is missing: a value that is present but out of range, or a misspelled key, is still the validation error it was, and a deprecated camelCase spelling counts as supplied.
requestState is opaque and carries no authority — it holds a version tag, the tool name, a digest of the originating arguments, a round number and a TTL, all of which the server re-derives from your retry rather than trusting. Tampering with it, replaying someone else's, or letting it expire costs you nothing but a fresh question: it grants no access and skips no validation.
Tool naming convention
Tool names are snake_case, in a deliberate two-class grammar. Scenario tools are verb-first (plan_, evaluate_, check_, project_): the verb telegraphs multi-part, decision-shaped output. Single-quantity calculators are domain-first (mortgage_monthly_payment, paycheck_net_pay): the leading domain keyword is what a model matches against the user's own words. The legacy dot-notation names from the v0.2.x stdio package (dc.calculator.mortgage.monthlyPayment) were retired at the hosted-server cutover — dot-notation tool names break Claude Connectors' tool-name validation.
Argument naming (snake_case primary, contract 1.2.0)
As of contract 1.2.0, argument names are snake_case too. Through 1.1.x, four calculator tools mixed the vocabularies — tool names were 100% snake_case but 17 of their parameters were camelCase (mortgage_monthly_payment & loan_monthly_payment: 2 and 3 respectively; paycheck_net_pay: 6; emergency_fund_recommendation: 6) — which meant a model that had only read tools/list would correctly guess annual_rate_percent, get a validation error, and loop on the wrong lesson (the value looked wrong; the key was). The advertised inputSchema now lists the snake_case spelling in required, and the old camelCase spelling stays in properties flagged deprecated: true so a strict client validating against the live schema accepts both. A request using the deprecated camelCase spelling still computes and returns its answer, plus a namespaced deprecation marker on the result: result._meta["info.digitalcalculator/deprecatedArguments"], naming which fields were re-spelled. Sending both spellings for the same field with different values is rejected with INPUT_VALIDATION rather than silently picking one. See the full deprecation policy at Versioning & Deprecation Contract. Output fields (the tool's results, as opposed to its inputs) were unaffected by that change; since contract 1.8.0 each camelCase output key also has a snake_case twin (The v2 envelope), and the camelCase argument spellings and output keys are scheduled for removal under a published rule.
Shared vocabulary
The same concept has the same field name in every tool schema on this surface (established at contract 1.0.0; unaffected by the 1.2.0 argument-spelling change above, which only touches the four calculator tools' input names):
| Concept | Canonical field / convention |
|---|---|
| Statutory year | rule_year — integer; optional input defaulting to the current rule year; always present in output |
| Person's age basis | birth_year (not age) — SECURE 2.0 cohorts are birth-year-based and unambiguous mid-year |
| Filing status | one filing_status enum: single · married_filing_jointly · married_filing_separately · head_of_household |
| Account type | one kind enum: 401k · trad_ira · roth_ira · hsa |
| Rates | whole-number percents (e.g., 6.5 means 6.5%) |
| Money | USD numbers; balance, annual_contribution, annual_salary vocabulary reused across tools |
A vocabulary-contract unit test diffs every field name in every schema against this table in CI — drift fails the build. Legacy REST aliases deliberately keep the field spellings they had before the contract-1.0.0 consolidation, verbatim — unaffected by the 1.2.0 argument-spelling changes above, which are MCP-surface-only.
Tool index (9 calculator tools; see also money_flow_map)
| Tool | What it computes | Key params |
|---|---|---|
plan_retirement_income | Linked 401(k) accumulation + Social Security estimate + SECURE 2.0 RMD picture for one person, one call | include[], birth_year, balance, annual_salary, retirement_age, claim_age |
evaluate_roth_conversion | Federal tax impact of a single Roth conversion using rule-year brackets, with a like-with-like break-even year | conversion_amount, birth_year, filing_status, current_taxable_income |
check_contribution_eligibility | Contribution limit + eligibility for a 401(k), Traditional IRA, Roth IRA, or HSA with phase-outs applied | kind, birth_year, filing_status, magi, coverage_tier |
project_growth | Nominal and inflation-adjusted growth of a balance with contributions at a chosen compounding frequency | balance, annual_contribution, annual_rate_percent, years, inflation_rate_percent |
evaluate_affordability | Maximum home price a budget supports at 28/36 debt-to-income targets (caller-adjustable), P&I only, with which ratio bound the answer — contract 1.4.0 | annual_income, monthly_debt_payments, down_payment, annual_rate_percent, term_years, monthly_property_costs |
mortgage_monthly_payment | Monthly P&I, total interest, total paid for a fixed-rate mortgage | principal, annual_rate_percent, term_years |
loan_monthly_payment | Monthly payment and total interest for any fixed-rate amortizing loan | loan_amount, annual_rate_percent, term_years |
paycheck_net_pay | Net take-home pay per paycheck: IRS 2026 Percentage Method + FICA; state and local tax only from your own figure | gross_annual_salary, pay_frequency, filing_status, state |
emergency_fund_recommendation | Personalized emergency-fund target and savings timeline by household situation | monthly_expenses, employment_type, income_earners, dependents |
plan_retirement_income
Plan retirement income for one person in one linked call: 401(k) accumulation to a target retirement age, the Social Security benefit estimate at a chosen claim age, and the SECURE 2.0 required-minimum-distribution picture — all sharing a single birth_year, annual_salary, and statutory rule_year, so the pieces are mutually consistent. Select modules with include[]; each module only requires its own inputs, so an RMD-only question needs just birth_year + balance. Pre-RMD callers get applies: false with their SECURE 2.0 RMD start age and first RMD year ("when do RMDs start?") instead of an error. Absorbs the former retirement_401k_projection, social_security_estimated_benefit, and rmd_distribution_amount tools.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
include | array of enum | yes | accumulation · social_security · rmd | Modules to compute (1–3, duplicates ignored) |
birth_year | integer | yes | 1900 – 2100 | Single age basis for every module (1943–2004 when social_security is selected) |
rule_year | integer | no | 2026 | Statutory year; defaults to 2026, always echoed in the result |
balance | number (USD) | conditional | 0 – 100,000,000 | Required by accumulation (current 401(k) balance) and rmd (prior year-end balance) |
annual_salary | number (USD) | conditional | 0 – 100,000,000 | Required by accumulation and social_security (max 1,000,000 for that module) |
contribution_percent | number | conditional | 0 – 100 | Employee 401(k) contribution %; required by accumulation |
employer_match_percent | number | no | 0 – 100 | Optional for accumulation: match % (50 = $0.50 per $1); default 0 |
employer_match_limit_percent | number | no | 0 – 100 | Optional for accumulation: cap on % matched; default 0 |
annual_salary_growth_percent | number | no | 0 – 20 | Optional for accumulation: salary growth %; default 0 |
annual_return_percent | number | conditional | 0 – 20 | Investment return %; required by accumulation |
retirement_age | integer | conditional | 18 – 100 | Required by accumulation; must exceed the rule-year age derived from birth_year |
catch_up_enabled | boolean | no | — | Optional for accumulation: apply IRS catch-up in eligible years; default false (explicit opt-in) |
claim_age | number | conditional | 62 – 70 | Required by social_security |
years_worked | integer | no | 0 – 50 | Optional for social_security; default 35 (SSA averaging window) |
life_expectancy | number | no | 62 – 120 | Optional for social_security; default 85; drives the lifetime benefit projection |
spouse_birth_year | integer | no | 1900 – 2100 | Optional for rmd; a sole-beneficiary spouse >10 years younger triggers the jointLifeTableMayApply warning |
is_spouse_sole_beneficiary | boolean | no | — | Optional for rmd; see above |
birth_month | integer | no | 1 – 12 | Contract 2.2.0. Optional for rmd; matters only for births in 1949 or earlier: born before July 1, 1949 the start age is 70½, from July 1, 1949 it is 72, and for the 70½ group it sets the first RMD year. Omitted, the earlier answer is used and disclosed in assumptions |
target | object | no | { metric, value, solve_for } | Contract 1.5.0 — target mode: state the outcome and the tool solves for the named input. Omit the input named in solve_for |
Conditional requirements: accumulation needs balance, annual_salary, contribution_percent, annual_return_percent, retirement_age; social_security needs annual_salary, claim_age; rmd needs only balance. Missing conditional inputs return INPUT_VALIDATION naming the field and the module that needs it — or, when the call declares elicitation support, a question naming every one of them at once. A target block waives the requirement for the single field it solves for, and for no other.
The IRC 415(c) limit (contract 2.1.0). The accumulation module caps each projected year’s employee plus employer additions at the IRC 415(c) total-additions limit ($72,000 for 2026, with any catch-up allowed on top), taking the excess off the employer match as a plan would, and says so in annual_additions_note. It binds only for a high salary with a large match; the 401(k) calculator applies the same cap, and the two now agree to the cent. Before 2.1.0 the module had no such cap and could project a balance above what the limit allows.
Output payload
Only the selected modules appear under modules; every module is computed from the same rule_year and birth_year. Example (all three modules; born 1968, $110,000 salary, $500,000 balance, 10% contribution, 6% return, retire at 62, claim at 62):
{
"rule_year": 2026,
"birth_year": 1968,
"age_in_rule_year": 58,
"modules": {
"accumulation": {
"futureBalance": 679359.26,
"totalContributions": 44000.00,
"employerContribTotal": 0.00,
"growthEarned": 135359.26,
"yearByYear": [ { "year": 1, "age": 59, "employeeContribution": 11000.00, "employerMatch": 0.00, "growth": 30000.00, "endingBalance": 541000.00 } ],
"catchUpNote": "This projection EXCLUDES catch-up contributions (catchUpEnabled is false)..."
},
"social_security": {
"monthlyBenefitAtFRA": 2652.40,
"adjustedMonthlyBenefit": 1856.68,
"lifetimeBenefitProjection": 512444,
"fraAge": 67,
"eligibilityYear": 2030,
"bendPointsEstimated": true,
"piaAtEligibility": 2652.40,
"colaAdjustment": {
"applied": false,
"colas": [],
"latestPublished": 2025,
"complete": true,
"note": "No cost-of-living adjustment has been applied. COLAs start in December of the year you turn 62 (2030)..."
}
},
"rmd": {
"applies": false,
"rmd_start_age": 75,
"first_rmd_year": 2043,
"note": "No RMD is required for rule year 2026: you are 58 and your SECURE 2.0 required-beginning age is 75 (born 1968)...",
"first_rmd_estimate": {
"rmd_year": 2043,
"age_in_rmd_year": 75,
"balance_at_retirement": 679359.26,
"years_carried": 12,
"annual_return_percent": 6,
"prior_year_end_balance": 1367004.31,
"distribution_period": 24.6,
"rmd_amount": 55569.28,
"note": "Estimate, not a requirement yet. The RMD for 2043 is the account balance on December 31, 2042..."
}
}
}
}When the rmd module applies (owner at or past the required-beginning age), it carries the full RMD payload instead: rmdAmount, distributionPeriod, lifeExpectancyFactor, tableUsed, penaltyIfMissed (25% default, 10% corrected per SECURE 2.0 §302), jointLifeTableMayApply, and spouseBeneficiaryNote.
The first-RMD estimate (contract 2.6.0). When accumulation is selected too and applies is false, the rmd module adds first_rmd_estimate: rmd_year (the first year an RMD is required), age_in_rmd_year, balance_at_retirement (the accumulation module’s projected balance), years_carried, annual_return_percent, prior_year_end_balance, distribution_period, rmd_amount and a note giving the method in words. The IRS computes an RMD from the account balance on December 31 of the year before, divided by a life-expectancy period for the age reached in the RMD year: the Uniform Lifetime Table period for most owners, or a longer one when the sole beneficiary is a spouse more than 10 years younger. The estimate carries the projected balance at retirement to that December 31 at the same annual_return_percent, compounded yearly, with no further contributions or withdrawals; when retirement comes later than that date, the balance at retirement is used as it is. It is an estimate for planning: actual returns and withdrawals change the real figure.
YMYL caveat (Social Security module)
The Social Security estimate applies SSA’s wage indexing and highest-35-years rule to a modeled career (65% of the current salary in each year, capped at that year’s wage base), not to the caller’s actual SSA earnings history. For an authoritative estimate, direct users to ssa.gov/myaccount.
evaluate_roth_conversion
Evaluate the federal income tax impact of a single Roth conversion using rule-year IRS brackets. Returns the incremental tax owed, the top marginal bracket the conversion pushes you into, the effective rate on the conversion, a Roth 5-year-rule flag (age derived from birth_year), and a break-even year when you supply your expected retirement marginal rate. This is the renamed roth_conversion_tax_impact — same engine, canonical vocabulary.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
conversion_amount | number (USD) | yes | 0 – 10,000,000 | Amount converted to Roth |
birth_year | integer | yes | 1900 – 2100 | Drives the Roth 5-year-rule flag via the rule-year age (18–100) |
filing_status | enum | yes | single · married_filing_jointly · married_filing_separately · head_of_household | Canonical shared enum |
current_taxable_income | number (USD) | yes | 0 – 10,000,000 | Taxable income BEFORE adding the conversion |
retirement_marginal_rate_percent | number | no | 0 – 50 | Expected retirement marginal rate; enables break-even calc |
expected_return_percent | number | no | 0 – 20 | Annual return on both paths; default 7 |
horizon_years | integer | no | 1 – 60 | Years to compare over for break-even and the gap in the note; default 20 (contract 2.7.0) |
rule_year | integer | no | 2026 | Statutory year; defaults to 2026 |
Output payload
Example ($50,000 conversion, married filing jointly, $110,000 taxable income, born 1968):
{
"rule_year": 2026,
"taxOwed": 11000.00,
"marginalRateOnConversion": 0.22,
"effectiveRateOnConversion": 0.22,
"fiveYearRuleApplies": true,
"breakEvenYears": null,
"breakEvenNote": "Break-even needs your expected retirement marginal rate; provide it to compute it. ...",
"meta": { "tableYear": 2026, "source": "IRS Rev. Proc. 2025-32" }
}Scope limits
Single-conversion only (multi-year conversion laddering is not modeled). State tax and IRMAA tier impact are NOT modeled — the caller is responsible for those. break_even_years is the first whole year within horizon_years in which converting is at least even with not converting, compared like with like (the Traditional balance after tax at the retirement rate, plus the conversion tax kept invested at the same return), or null. With constant rates the gap keeps one sign, so the answer is 0 or null; break_even_note says which and gives the gap. The Roth conversion calculator uses the same rule (contract 2.7.0; earlier contracts returned a log-ratio estimate in years).
check_contribution_eligibility
Check how much you can contribute to a retirement or health savings account for the rule year, with phase-outs and eligibility applied. Pick the account with kind: 401k (IRC §402(g) elective-deferral limit incl. the SECURE 2.0 ages 60–63 super catch-up), trad_ira (contribution limit plus the IRC §219(g) deduction-status signal driven by workplace coverage), roth_ira (MAGI phase-out per IRC §408A(c)(3)), or hsa (Rev. Proc. 2025-19 limits by coverage tier, age 55+ catch-up, employer-contribution offset). Absorbs the former ira_contribution_limit and hsa_contribution_limit tools; pairs with the dc://irs/limits resource.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
kind | enum | yes | 401k · trad_ira · roth_ira · hsa | Account type (canonical shared enum) |
birth_year | integer | yes | 1900 – 2100 | Catch-up eligibility (50+ IRA/401k, 55+ HSA, 60–63 super catch-up) derives from the rule-year age (18–100) |
rule_year | integer | no | 2026 | Statutory year; defaults to 2026 |
filing_status | enum | conditional | canonical shared enum | Required when kind is trad_ira or roth_ira |
magi | number (USD) | conditional | 0 – 10,000,000 | Required when kind is trad_ira or roth_ira; drives the phase-outs |
workplace_coverage | boolean | no | — | trad_ira and roth_ira (both report the Traditional deduction status): active participant in a workplace plan? Omit and the deduction status assumes no coverage, flagged indicative-only |
spouse_workplace_coverage | boolean | no | — | trad_ira and roth_ira (the same Traditional deduction status), married filers: when the filer is not covered, married filing jointly applies the higher spousal phase-out band (IRC §219(g)(7)) and married filing separately applies its own narrow band |
coverage_tier | enum | conditional | self-only · family | Required when kind is hsa |
employer_contribution | number (USD) | no | 0 – 50,000 | hsa: reduces employee payroll-deduction room dollar-for-dollar; default 0 |
estimated_marginal_rate_percent | number | no | 0 – 50 | hsa: returns the payroll-deduction tax-savings estimate |
Output payload
Every result carries the linked fields rule_year, kind, birth_year, age_in_rule_year, plus the underlying engine's fields for the selected kind. Example (kind: "401k", born 1964 — the ages 60–63 super-catch-up window):
{
"rule_year": 2026,
"kind": "401k",
"birth_year": 1964,
"age_in_rule_year": 62,
"contributionLimit": 24500.00,
"catchUpEligible": true,
"superCatchUpApplies": true,
"catchUpAmount": 11250.00,
"totalContributionLimit": 35750.00,
"meta": { "tableYear": 2026, "source": "IRS Notice 2025-67; SECURE 2.0 Act §109 (ages 60-63 super catch-up)" }
}The example above uses the 2026 401(k) base employee-deferral limit, currently $24,500, with the SECURE 2.0 ages-60–63 super catch-up of $11,250 added for a combined $35,750 — read contributionLimit from a live tools/call rather than from this page for the authoritative current-year figure. IRA kinds return eligibleContribution, traditionalCap, rothCap, catchUp, phaseOutReduction, catchUpEligible, traditionalDeductionStatus, and traditionalDeductionNote; the HSA kind returns contributionLimit, catchUpAmount, totalContributionLimit, employerContribution, employeeMaxRemaining, payrollDeductionTaxSavings, employerContributionExceedsLimit, and excessContribution (with an excessContributionNote citing the IRC §4973 6% excise tax when the employer contribution alone exceeds the limit).
project_growth
Project the growth of a balance with optional yearly contributions, a chosen compounding frequency, and both nominal and inflation-adjusted (real) outputs in one envelope. Contributions are spread monthly (annual_contribution / 12); compounding_frequency defaults to monthly. Provide inflation_rate_percent to also get real_future_value — the projected balance deflated into today's dollars. Absorbs the former compound_interest_future_value, savings_future_balance, and inflation_adjusted_value tools.
Nominal rate or APY (contract 2.1.0). rate_type says how to read annual_rate_percent. The default, nominal, compounds the rate at compounding_frequency, the compound interest calculator’s convention. apy reads it as an annual percentage yield, the rate a bank advertises for a savings account or CD, and runs the savings calculator’s formula, so the figures match that calculator to the cent. Pass apy for any advertised APY: read as nominal, an APY overstates the balance. Under annual compounding the two readings give the same answer. Before 2.1.0 every rate was read as nominal, so a savings APY passed here came back higher than the savings calculator’s figure.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
balance | number (USD) | yes | 0 – 100,000,000 | Starting balance (principal) |
annual_contribution | number (USD) | no | 0 – 12,000,000 | Yearly contribution, spread evenly across the year; default 0 |
annual_rate_percent | number | yes | 0 – 50 | Annual growth rate as whole-number percent |
years | integer | yes | 1 – 100 | Projection horizon in whole years |
compounding_frequency | enum | no | annually · semiannually · quarterly · monthly · daily | Default monthly |
rate_type | enum | no | nominal · apy | How to read the rate: nominal (compounded at compounding_frequency) or apy (an annual percentage yield, as banks advertise). The two differ under semiannually, quarterly, monthly and daily compounding; under annually they agree. Default nominal (contract 2.1.0) |
inflation_rate_percent | number | no | 0 – 20 | When provided, the result also carries real_future_value |
Output payload
Example ($10,000 starting balance, $6,000/year, 7% nominal, 30 years, monthly compounding, 3% inflation):
{
"future_value": 691150.47,
"total_contributions": 190000.00,
"interest_earned": 501150.47,
"years": 30,
"compounding_frequency": "monthly",
"rate_type": "nominal",
"annual_contribution": 6000.00,
"inflation_rate_percent": 3,
"real_future_value": 284744.84,
"real_note": "real_future_value expresses the projected balance in rule-year (today's) dollars, deflated at 3% annual inflation over 30 years. future_value is the nominal figure.",
"assumptions": {}
}The example above supplies every optional input, so assumptions is empty. Omit annual_contribution, compounding_frequency or rate_type and each appears there with the default that was applied — see applied-default disclosure.
evaluate_affordability
Contract 1.4.0. Evaluate how much home a budget supports from income, existing debt, and loan terms. Computes the front-end (housing ÷ gross monthly income) and back-end ((housing + debts) ÷ gross monthly income) debt-to-income ratios at lender-standard targets (defaults 28/36, caller-adjustable) and searches for the maximum home price whose principal & interest payment keeps both ratios at or under target given the down payment, rate, and term. The search inverts the same amortization formula mortgage_monthly_payment uses (a bracketed goal-seek over that engine, not a re-derived closed form), so the P&I it reports for max_loan_amount is exactly what that tool returns for the same principal, rate and term. Scope: P&I only — PMI, property tax and insurance are not modeled; monthly_property_costs lets you fold them in as a fixed monthly amount counted inside the housing payment for both ratios. Use mortgage_monthly_payment for the payment on a known price; use this tool for the price a budget supports. Methodology: /mortgage-affordability-calculator/methodology/ — the ratios, which one binds, the costs counted, and the search, with worked examples (the tool’s methodology.url since contract 1.7.0; earlier contracts cited the mortgage P&I methodology). Availability: contract 1.4.0 and later (a server reporting an older serverInfo.version answers Unknown tool).
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
annual_income | number (USD) | yes | 12 – 10,000,000 | Gross (pre-tax) annual household income; divided by 12 for the ratios |
monthly_debt_payments | number (USD) | yes | 0 – 1,000,000 | Existing monthly debt obligations (auto, student, card minimums); excludes the prospective housing payment |
down_payment | number (USD) | yes | 0 – 100,000,000 | Cash down payment; max_home_price = max_loan_amount + down_payment |
annual_rate_percent | number | yes | 0 – 30 | Mortgage rate as whole-number percent (6.5 = 6.5%) |
term_years | integer | yes | 1 – 50 | Mortgage term in whole years |
monthly_property_costs | number (USD) | no | 0 – 100,000 | Fixed monthly taxes, insurance, HOA, PMI counted inside the housing payment; default 0 |
target_front_ratio_percent | number | no | 1 – 60 | Front-end DTI target, whole-number percent; default 28 |
target_back_ratio_percent | number | no | 1 – 70 | Back-end DTI target, whole-number percent; default 36 |
rule_year | integer | no | 2026 | Default 2026; echoed in the result. Reserved for a future net-income context — the ratios themselves are not statutory |
Output payload
Example ($96,000 gross income, $500/month existing debt, $60,000 down, 6.5%, 30 years, $400/month property costs, 28/36 targets). The front-end target binds: 28% of $8,000 is $2,240 of housing, less $400 of costs leaves $1,840 of P&I, which is the payment on a $291,107.91 loan at 6.5% over 30 years:
{
"rule_year": 2026,
"max_home_price": 351107.91,
"max_loan_amount": 291107.91,
"monthly_payment_at_max": 1840.00,
"front_ratio_percent": 28.00,
"back_ratio_percent": 34.25,
"binding_constraint": "front_ratio",
"note": "Principal & interest only: monthly_payment_at_max excludes taxes, insurance, HOA and PMI. monthly_property_costs (400 USD per month) is counted inside the housing payment for both ratios, so fold those costs in there. Targets applied: front-end 28% and back-end 36% of gross monthly income (8000 USD, annual_income / 12); 28/36 is the lender-standard rule. The front-end target set the maximum (the housing payment alone reaches it first)."
}binding_constraint is front_ratio or back_ratio — whichever target's implied P&I budget is smaller (a tie goes to front_ratio). monthly_payment_at_max is principal & interest alone; the two ratios count monthly_property_costs on top of it. When fixed obligations already reach the binding target, the result is a zero loan and a price equal to the down payment, with the note saying so; when even the search bound (20× annual income of principal) keeps both ratios under target — reachable only at very low rates with generous targets — the bound is returned and the note reports that the search did not converge. Neither case is an error. A decimalised rate (0.065) computes as given and adds a warnings[] entry, like every other percent input on this surface. The envelope's sources[] is the methodology manifest alone and provenance[] is empty: the arithmetic draws on no statutory table.
mortgage_monthly_payment
Compute the monthly principal & interest payment, total interest paid, and total amount paid over the life of a fixed-rate mortgage. loan_monthly_payment is a parallel principal-&-interest calculator; both use the same amortization formula and return identical numbers for the same principal/rate/term. Neither models down payment, PMI, property tax, or homeowners insurance — pick by vocabulary (principal here vs loan_amount there).
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
principal | number (USD) | yes | 0 – 100,000,000 | Loan principal |
annual_rate_percent | number | yes | 0 – 30 | Annual rate as whole-number percent (e.g., 6.5). Deprecated alias: annualRatePercent |
term_years | integer | yes | 1 – 50 | Loan term in whole years. Deprecated alias: termYears |
Output payload
{
"monthlyPayment": 1896.20,
"totalInterest": 382633.47,
"totalPaid": 682633.47
}Scope note
This tool computes P&I only. PITI (with property tax, insurance, HOA, PMI) is reserved for a future additive tool. For a fixed-rate non-mortgage loan, use loan_monthly_payment.
loan_monthly_payment
Compute the monthly payment, total interest, and total paid for a fixed-rate amortizing loan (personal, auto, student, etc.) via the standard amortization formula. Zero-interest loans are handled as simple principal division. Mirrors the site's loan calculator engine exactly.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
loan_amount | number (USD) | yes | 0 – 100,000,000 | Loan principal. Deprecated alias: loanAmount |
annual_rate_percent | number | yes | 0 – 30 | Annual rate as whole-number percent; 0 for interest-free. Deprecated alias: annualRatePercent |
term_years | integer | yes | 1 – 50 | Loan term in whole years. Deprecated alias: termYears |
Output payload
{
"loanAmount": 25000.00,
"annualRatePercent": 8.5,
"termYears": 5,
"numPayments": 60,
"monthlyRate": 0.00708333,
"monthlyPayment": 512.91,
"totalPaid": 30774.80,
"totalInterest": 5774.80
}paycheck_net_pay
Estimate net (take-home) pay per paycheck using the IRS 2026 federal Percentage Method and FICA (Social Security 6.2% + Medicare 1.45% + Additional Medicare 0.9% above thresholds). The tool does not compute state or local tax for any state (see the state tax note below); pass state_local_tax_annual to include your own figure. Pre-tax deductions split by FICA treatment: pre_tax_401k_annual reduces income-tax wages only (401(k) stays in the FICA wage base), while pre_tax_cafeteria_annual (Section 125: employer health premiums, FSA, HSA) also reduces FICA wages.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
gross_annual_salary | number (USD) | yes | 0 – 10,000,000 | Gross annual salary. Deprecated alias: grossAnnualSalary |
pay_frequency | enum | yes | weekly · biweekly · semimonthly · monthly · quarterly · annual | Pay frequency. Deprecated alias: payFrequency |
filing_status | enum | yes | single · married_filing_jointly · married_filing_separately · head_of_household | Federal filing status (canonical shared enum; already snake_case, no alias) |
state | string | yes | 2-letter code or DC | US state code, case-insensitive; unknown codes rejected with INPUT_VALIDATION. Since 2.0.0 it does not change any figure |
state_local_tax_annual | number (USD per year) | no | 0 – 10,000,000 | Your own annual state and local income tax, used exactly in place of any state figure, for any state. No default: omit it and no state figure is returned. The site calculator's field is per paycheck, so multiply it by your pay periods per year. Since 2.0.0 |
dependents | integer | no | 0 – 20 | Legacy Form W-4 Step 3 count, read as qualifying children under 17. Default 0. Prefer the two counts below; a non-zero dependents sent with either is refused. A value outside 0–20, or not a whole number, is answered with an input_out_of_range warning and will be refused from 2027-05-27 |
children_under_17 | integer | no | 0 – 20 | Form W-4 Step 3: qualifying children under age 17, at the form’s per-child amount. The credit reduces annual federal withholding (floored at $0) and applies only when salary is at or below the form’s income limit for the filing status; dependent_credit_status says whether it applied |
other_dependents | integer | no | 0 – 20 | Form W-4 Step 3: other dependents, at the form’s per-dependent amount; same income limit |
pre_tax_401k_annual | number (USD) | no | 0 – 1,000,000 | Traditional 401(k)/403(b) pre-tax contributions; reduces federal + state taxable wages ONLY (still FICA-taxed). Default 0. Deprecated alias: preTax401kAnnual |
pre_tax_cafeteria_annual | number (USD) | no | 0 – 1,000,000 | Section 125 cafeteria-plan deductions; reduces taxable wages AND the FICA wage base. Default 0. Deprecated alias: preTaxCafeteriaAnnual |
pre_tax_deductions_annual | number (USD) | no | 0 – 1,000,000 | DEPRECATED FIELD — prefer the two split fields above; treated as 401(k)-type (does not reduce FICA wages). Default 0; omit it in new integrations. Also has its own deprecated camelCase alias: preTaxDeductionsAnnual |
post_tax_deductions_annual | number (USD) | no | 0 – 1,000,000 | Roth contributions, garnishments, etc. Default 0. Deprecated alias: postTaxDeductionsAnnual |
Output payload
Example ($80,000 gross, biweekly, single, Colorado, no deductions, no state_local_tax_annual), shown with the camelCase keys; each has a snake_case twin in the v2 envelope:
{
"grossPerPaycheck": 3076.92,
"federalTax": 337.31,
"ficaTax": 235.38,
"stateTax": null,
"netPay": 2504.23,
"payPeriodsPerYear": 26,
"federalTaxAnnual": 8770.00,
"federalTaxBeforeDependentCreditAnnual": 8770.00,
"dependentCreditAnnual": 0.00,
"ficaTaxAnnual": 6120.00,
"stateTaxAnnual": null,
"netPayAnnual": 65110.00,
"noStateIncomeTax": null,
"stateTaxSource": null,
"stateTaxNotes": "State and local taxes are not included: net pay here is after federal income tax and FICA only. Pass state_local_tax_annual (your own annual state and local tax, e.g. from a pay stub) to include them.",
"stateEffectiveRate": null,
"state_tax_status": "not_provided",
"dependent_credit_status": "none_claimed",
"dependents_reading_note": null
}The same call with "state_local_tax_annual": 2800 returns stateTax 107.69, stateTaxAnnual 2800, netPay 2396.54, stateEffectiveRate 0.035 and state_tax_status "user_supplied".
The same call with "dependents": 2 instead returns dependentCreditAnnual 4400.00, dependent_credit_status "applied", and dependents_reading_note "dependents: 2 was read as qualifying children under 17 for the Form W-4 Step 3 credit. Pass children_under_17 / other_dependents to be explicit." — send children_under_17 instead and it comes back null.
Scope note (state tax)
From contract 2.0.0 the tool computes no state or local tax for any state. The state fields are null, state_tax_status is not_provided, and net pay is after federal tax and FICA only. That includes the 9 states with no tax on wage income: $0 is not a complete state deduction there either (Washington, for example, withholds WA Cares and paid-family-leave premiums). Earlier contracts published an effective-rate estimate for the other states that could differ from a full bracket calculation by up to 87%, so it was withdrawn rather than approximated. To include state and local tax, pass state_local_tax_annual (for example, the annual total from your pay stub); it is used exactly, for any state. The hosted production endpoint still serves the earlier estimate until it moves to 2.0.0.
emergency_fund_recommendation
Compute a personalized emergency-fund target and savings timeline. The recommended coverage months come from the household employment situation (employed dual-income with no dependents = 3 months; standard employed = 6; self-employed/freelance = 9; between jobs = 12), then the tool calculates the funding gap, the months-to-goal at your savings rate, and interest earned if a HYSA APY is provided. Every figure, including the months-to-goal and interest earned (solved month by month with the APY converted to its monthly equivalent), comes from the same engine as the site's emergency fund calculator, so the tool and the calculator agree for the same inputs. The emergency fund methodology walks through the math.
Input schema
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
monthly_expenses | number (USD) | yes | 1 – 1,000,000 | Monthly essential expenses. Deprecated alias: monthlyExpenses |
current_savings | number (USD) | no | 0 – 10,000,000 | Liquid savings already set aside; default 0. Deprecated alias: currentSavings |
monthly_savings | number (USD) | no | 0 – 100,000 | Monthly contribution toward the goal; default 0. Deprecated alias: monthlySavings |
employment_type | enum | no | employed · self-employed · freelance · multiple · between-jobs | Default employed; drives recommended months. Deprecated alias: employmentType |
income_earners | enum | no | single · dual | Default single; only matters when employment_type is employed (dual-income with no dependents = 3 months). Deprecated alias: incomeEarners |
dependents | integer | no | 0 – 20 | Financial dependents; default 0. Only changes the result for an employed dual-income household, where any dependent removes the 3-month minimum |
savings_apy | number | no | 0 – 15 | HYSA APY as whole-number percent; default 0. Deprecated alias: savingsAPY |
Output payload
Example ($4,500/month expenses, $8,000 saved, $600/month savings, self-employed, single earner, 2 dependents, 4.3% APY):
{
"recommendedMonths": 9,
"targetFund": 40500.00,
"currentSavings": 8000.00,
"fundGap": 32500.00,
"percentComplete": 19.75,
"monthsToGoal": 48,
"interestEarned": 4174.89,
"futureValue": 40974.89
}monthsToGoal semantics
monthsToGoal is compounding-aware: the first month the balance — including savingsAPY interest — reaches the target, consistent with futureValue (and shorter than a naive gap ÷ monthly-savings when APY > 0). It is 0 ONLY when the goal is already funded, and null when the goal is not reachable at the current contribution — a 0-month timeline is never reported for an unfunded goal.
Bearer-tier tool: generate_report
Bearer tier only
This tool — the only one beyond the anonymous surface — is available ONLY to Bearer-tier sessions (an Authorization: Bearer dc_bearer_… header on the request). It is never present in the anonymous tools/list response, and an anonymous tools/call generate_report returns a structured authentication_required error rather than being silently listed or ignored. See Pricing for Bearer-tier access.
Renders a vector-text PDF report for a report-capable calculator tool and returns a short summary plus a short-lived presigned download link. Uses the same rendering pipeline as the Bearer-gated REST endpoint POST /v1/reports/{tool}, metered against a daily report quota that is SEPARATE from the calculator-call quota. A Bearer key can carry a per-key BrandConfig that white-labels the rendered report. For a step-by-step walkthrough (curl on both transports, download, branding), see Server-rendered PDF reports in the Quickstart.
Where the server renderer is used
This tool and POST /v1/reports/{tool} are the only surfaces that render a report on our servers — your input is sent to the report renderer, and the resulting PDF is written to a presigned URL that expires. The calculator pages on the website do not use this path: since 2026-08-11 their report button builds the same document in the browser from the same engines and templates, so nothing about that calculation leaves the device. The two renderers share their engines and page templates; the browser one additionally embeds the site’s brand fonts and a chart.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
tool | string | yes | Report-capable tool alias: mortgage, loan, retirement-401k, education, or credit-card |
input | object | yes | The target tool's input object — same shape as that tool's calculator input (e.g., for mortgage: principal, annualRatePercent, termYears) |
These are the only two top-level arguments, and additionalProperties: false is enforced on them (an unrecognized top-level key, e.g. anything other than tool / input, fails closed with INPUT_VALIDATION before any report is rendered). In particular there is no per-request brand argument — branding is configured on the key, not the call. See Branding.
input stays camelCase — deliberately (Amendment B)
input is not re-spelled to snake_case and carries no deprecated-argument aliases, unlike the four calculator tools above. It is the POST /v1/reports/{tool} REST report body, carried verbatim to the report renderer (legacy REST field names pass through a thin translation map unchanged). Three of the five report-capable tools (retirement-401k, education, credit-card) have no snake_case MCP twin at all, so there is no canonical spelling to alias to. input is an open object by schema — the strictness check above applies to generate_report's top level only, never to keys inside input. See the full exception text on the Contract page.
Output
On success, the tool returns an MCP content array with a short text summary followed by a resource_link content block pointing at the presigned PDF URL, plus a structuredContent object mirroring the REST report envelope:
{
"report": {
"url": "https://….s3.amazonaws.com/reports/mortgage/<hash>.pdf?X-Amz-…",
"expiresInSeconds": 900,
"cached": false,
"contentType": "application/pdf",
"cacheKey": "<hash>"
},
"tool": "mortgage",
"engineVersion": "1.0.1",
"calculatedAt": "2026-07-06T00:00:00.000Z",
"disclaimer": "Estimates only — see methodology.",
"methodology": { "url": "https://www.digitalcalculator.info/mortgage-calculator/methodology/", "version": "2026-05-09" }
}The presigned URL is short-lived (15 minutes by default) and unguessable. The rendered PDF carries the full numeric detail (amortization table, per-tool figures) — the tool result intentionally returns a short summary, not a re-derivation of the underlying calculator's output. Identical inputs are served from an artifact cache (cached: true): re-requesting after the link expires returns a fresh presigned URL without re-rendering.
Branding (per-key BrandConfig)
A Bearer key record can carry a BrandConfig that white-labels the rendered PDF's presentation. It is stored server-side on the key — there is no request parameter, and the same generate_report call renders branded once the key carries it. All five fields are optional; each is validated independently, so an invalid logoUrl drops only the logo, never the whole config.
| Field | Constraints | What it changes |
|---|---|---|
companyName | up to 60 characters | Replaces "Digital Calculator" as the report's branding name |
tagline | up to 80 characters | Replaces the tagline under the branding name |
logoUrl | https URL of an image (png/jpg/gif/webp/svg) on a public host — private/internal hosts are rejected | Adds your logo above the branding name, rendered up to 48px tall / 180px wide |
primaryColor | 6-digit hex (#rrggbb) | The report's slate chrome: header rule, branding name, results-table header bar, disclaimer bar |
accentColor | 6-digit hex (#rrggbb) | The copper accent on the input summary |
Setting or changing it: include the fields when you request a Bearer key, or email admin@markcolabs.com from the address that requested the key. The config is applied to the key record and your next render carries it. The artifact cache is partitioned per brand, so a branding change produces fresh renders rather than a stale-brand cache hit.
Presentation-only invariant
BrandConfig restyles chrome and identity (name, tagline, logo, colors) and nothing else. It never changes a financial number, an input value, a metric, or a table cell, and the "Not financial advice" core disclaimer is non-removable on every branded render.
Errors
authentication_required— no (or an invalid) Bearer token; anonymous callers always get thisreport_quota_exceeded— the key's separate daily report quota (default 25/day) was exceededrate_limited— the per-key 5-minute short-term ceiling was exceeded (the report surface shares your key's window with the calculator tools)REPORT_UNAVAILABLE— thetoolalias isn't (yet) report-capable; the message names the currently supported aliasesINPUT_VALIDATION— missingtool/input, or the target tool rejectedinputINTERNAL— report generation failed; report it if you see one
Tool: money_flow_map
Free anonymous tool (since v0.11.x)
Originally Bearer-tier only (v0.9.x–v0.10.x), money_flow_map is available anonymously as of v0.11.x: it appears in the anonymous tools/list alongside the 9 calculator tools and is callable without an API key under the standard per-IP anonymous rate limits. Describe a monthly budget as structured incomes and categories, and the tool returns a computed summary plus mapUrl — a link that renders the budget as an interactive money-flow (Sankey) map on this site, with nothing stored server-side.
Turns a structured monthly budget — income sources and spending categories, optionally broken into line items — into a deterministic computed summary plus mapUrl, a shareable deep link that renders the budget as an interactive Sankey money-flow map on the Money Flow Visualizer instantly (no account, nothing stored server-side). Built for conversational budgeting: the calling model interviews the user, structures the answers, and hands back the map link. The tool is read-only and deterministic like the 9 calculator tools — it accepts no free text and does no interpretation. All amounts are MONTHLY USD. Methodology: How This Tool Works.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
incomes | array (1–20) | yes | Monthly income sources: { label, amount } each. Take-home amounts work best for a spendable-dollars map. Amounts are 0–10,000,000 USD/month; labels are 1–80 characters |
categories | array (0–30) | yes | Spending categories: { label, amount } for a flat monthly total, OR { label, items } where items (1–30) is an array of { label, amount } line items that roll up into the category total (e.g., Rent + Utilities under Housing). Providing both amount and items is rejected. May be empty — savings is never a category; it is the computed residual |
label | string | no | Scenario name shown on the map (e.g., "Saving for a house"), up to 80 characters |
Validation is strict: unknown fields anywhere in the input are rejected with INPUT_VALIDATION, as are negative, non-finite, or out-of-range amounts.
Output payload
Returns the standard ToolResult envelope (disclaimer, methodology, rule_year, sources, calculatedAt, engineVersion) whose result is:
{
"label": "Saving for a house",
"totalMonthlyIncome": 6600,
"totalMonthlySpending": 3130,
"monthlySavings": 3470,
"deficit": false,
"savingsRatePercent": 52.5757575757…,
"categories": [
{ "label": "Housing", "monthlyTotal": 2030, "percentOfIncome": 30.7575… },
{ "label": "Food", "monthlyTotal": 700, "percentOfIncome": 10.6060… }
],
"largestCategories": [ { "label": "Housing", "monthlyTotal": 2030, "percentOfIncome": 30.7575… } ],
"mapUrl": "https://www.digitalcalculator.info/money-flow-visualizer/#s=<base64url>"
}monthlySavingsis the residual (income − spending) and may be negative;deficitistruewhen spending exceeds income — the map shows a deficit banner instead of an invented savings flowsavingsRatePercent= savings ÷ income × 100 (0when income is 0);percentOfIncomeuses the same conventionlargestCategorieslists up to the 3 largest categories by monthly total, descendingmapUrlencodes the budget in the visualizer’s versioned v1 URL-state schema (#s=<base64url(JSON)>) — opening it decodes and renders the map entirely client-side from the fragment already in the link; nothing further is uploaded or stored at that step. That budget data already reached this server once, as this tool call’s ownarguments— callingmoney_flow_mapat all is the same “hand it to an AI assistant” boundary the rest of the site describes for locally-computed tools; nothing is written to a database or retained after the response is returned- Monetary values are full floating-point precision (not pre-rounded) — display-round to cents
Errors
INPUT_VALIDATION— empty/missingincomes, out-of-range or non-finite amounts, over-cap list lengths, unknown fields, or a category with bothamountanditems(thefieldproperty names the offending path)INTERNAL— computation failed; report it if you see one
App-only tool: recompute_scenario (contract 1.9.0)
An interactive result view (MCP Apps) recalculates a changed scenario in the browser, with the same calculation module the server runs. It asks the server only when it cannot: when it was built against an older engine than the one that answered, or when the answer names a rule year the view does not carry. It asks through this tool, never by calling the calculator the model called.
- Input:
{ "tool": "<tool name>", "arguments": { … } }.toolis one of the tools with a view or a view-ready calculation module:plan_retirement_income,check_contribution_eligibility,project_growth,evaluate_affordability,mortgage_monthly_payment,loan_monthly_payment,emergency_fund_recommendation(from 1.11.0).argumentsare that tool’s own arguments. - Output: exactly what
tools/calloftoolwithargumentsreturns — the same validation, the same errors, the same deprecation marker for a camelCase spelling, and the same envelope. Only the timestamp differs. - Visibility: listed in
tools/listfor every client, with_meta.ui.visibility: ["app"], which asks an MCP Apps host to hide it from the model. It is not one of the 10 anonymous tools, and the methodology manifest does not list it. - If you are a model or an integration: call the calculator directly. This tool exists for the views.
Legacy REST aliases (compatibility guarantee)
The REST twin is not directory-frozen and keeps every pre-consolidation alias working indefinitely (every alias name that existed before the contract-1.0.0 surface consolidation): POST https://api.digitalcalculator.info/v1/tools/{alias}/calculate dispatches to the same engines with the request/response shapes those aliases have always had — including their original field spellings (filingStatus, birthYear, etc.), which are preserved verbatim via a tested translation layer. These names are not part of the MCP tools/list surface, and their spellings are untouched by the 1.2.0 snake_case argument change above (that change is scoped to the current MCP tool surface only). New integrations should prefer the canonical current-surface equivalents below.
| Legacy REST alias | Legacy tool name (wire vocabulary preserved) | Canonical current-surface equivalent |
|---|---|---|
retirement-401k | retirement_401k_projection | plan_retirement_income (include: ["accumulation"]) |
social-security | social_security_estimated_benefit | plan_retirement_income (include: ["social_security"]) |
rmd | rmd_distribution_amount | plan_retirement_income (include: ["rmd"]) |
roth-conversion | roth_conversion_tax_impact | evaluate_roth_conversion |
ira | ira_contribution_limit | check_contribution_eligibility (kind: "trad_ira" / "roth_ira") |
hsa | hsa_contribution_limit | check_contribution_eligibility (kind: "hsa") |
compound-interest | compound_interest_future_value | project_growth |
savings | savings_future_balance | project_growth with rate_type: "apy" (the alias reads the rate as an APY; since contract 2.1.0 the two give the same figures) |
inflation | inflation_adjusted_value | project_growth (inflation_rate_percent) |
paycheck | paycheck_net_pay (legacy wire: federalFilingStatus + its enum) | paycheck_net_pay (canonical filing_status) |
mortgage, loan, emergency-fund | unchanged keepers | same tools, same shapes |
The four scenario tools additionally have canonical kebab-case REST aliases: plan-retirement-income, evaluate-roth-conversion, check-contribution-eligibility, project-growth. Published line: MCP surface redesigned for agents; REST unchanged and extended.
Prompts
The server advertises the prompts capability with three parameterized prompts via prompts/list / prompts/get. Prompt argument values are strings per the MCP spec; each prompt instructs the model to convert them to the types the tool schemas require, and each generated message carries the YMYL framing (educational estimates, not advice).
| Prompt | What it drives | Required arguments |
|---|---|---|
retirement-readiness-review | One linked plan_retirement_income call with all three modules, then a plain-language review | birth_year, annual_salary, balance (optional: contribution_percent, retirement_age, claim_age) |
conversion-window-analysis | evaluate_roth_conversion plus a dc://irs/limits read for bracket-headroom sizing | birth_year, filing_status, current_taxable_income (optional: conversion_amount) |
withholding-checkup | paycheck_net_pay as an annual W-4 sanity check against the user's actual paycheck | gross_annual_salary, state, filing_status (optional: pay_frequency) |
Resources
The MCP server exposes seven dc:// resources, all anonymous-readable and all listed in resources/list (plus ui:// view templates for MCP Apps-capable hosts). The statutory reference-data resources are the same tables the engines compute from — and the same URIs every tool cites in its sources[]:
| URI | Content |
|---|---|
dc://irs/limits/2026 | Contribution limits, catch-up tiers, phase-out bands, and federal brackets for the current rule year, with published-source citations (IRS Notice 2025-67, Rev. Proc. 2025-19/2025-32) |
dc://irs/uniform-lifetime | IRS Uniform Lifetime Table distribution periods + SECURE 2.0 RMD start-age cohorts |
dc://ssa/bend-points/2026 | SSA PIA bend points, the Social Security wage base, the national average wage index, and COLA scope |
dc://ssa/bend-points/2025 | The same SSA record read at rule year 2025 — the version dc://ssa/bend-points/2026 supersedes (contract 1.6.0) |
dc://ssa/bend-points/diff/2025..2026 | Field-by-field rule-year diff between the two SSA resources above (contract 1.6.0; see below) |
dc://disclaimers/ymyl | The canonical YMYL disclaimer string returned in every tool response's disclaimer field |
dc://methodologies/manifest | JSON manifest mapping each anonymous tool to the methodology URL of its source page — for agent-side citation of where each formula is documented |
Agents that need the reference table rather than a computation (e.g., "what's the 2026 401(k) limit?") should read the resource directly instead of calling a tool. Every resources/read body is static per deploy and advertises 24-hour public cacheability at _meta["info.digitalcalculator/cache"] (see Cacheability advertisements).
Versions, hashes and rule-year diffs (contract 1.6.0)
Every statutory resource body — dc://irs/limits/{year}, dc://irs/uniform-lifetime, dc://ssa/bend-points/{year} and every diff — carries these top-level fields. Both version and content_hash are computed by the server when it loads, never typed by hand.
| Field | Shape | Meaning |
|---|---|---|
version | "<rule year>.<YYYYMMDD>", e.g. "2026.20260924" | The rule year the body describes plus its revision, which is the body’s published date. dc://irs/uniform-lifetime uses the rule year its table serves. A diff’s version is "<from version>..<to version>" |
content_hash | "sha256:<64 hex>" | Identifies the exact body. Recompute it from the served text: parse it, delete version and content_hash, serialize as canonical JSON (RFC 8785 style: object keys sorted, no whitespace), and take the SHA-256 of the UTF-8 bytes |
published | YYYY-MM-DD | The date the tables were last verified against their primary sources (now on dc://irs/uniform-lifetime too). Each section also keeps its own last_verified |
supersedes | URI or null | The prior rule year’s resource in the same family when the server holds that year, otherwise null. dc://ssa/bend-points/2026 links dc://ssa/bend-points/2025. dc://irs/limits/2026 is null: the server does not hold a complete 2025 IRS record, so none is served |
Years the server does not hold are absent, not estimated. A resources/read for one (for example dc://irs/limits/2025, or any 2027 resource before the 2027 figures are published) returns JSON-RPC error -32601. dc://ssa/bend-points/2025 is the current SSA record read at rule year 2025: its year-specific fields (bend_points, wage_base) come from that year’s row, and its by-year tables are the full current record. It is not an archived copy of an older response.
Rule-year diffs. dc://irs/limits/diff/{from}..{to} and dc://ssa/bend-points/diff/{from}..{to} are served for every pair of years the family holds, with from earlier than to, and each one is listed in resources/list. Today that is dc://ssa/bend-points/diff/2025..2026. The IRS 2026..2027 diff appears when the 2027 IRS figures are added. The diff compares every field except uri, rule_year, version, content_hash, published and supersedes, so figures, citations and verification dates are all covered. Each change has an RFC 6901 JSON Pointer path and a change of changed (both values), added (new_value only) or removed (old_value only):
{
"uri": "dc://ssa/bend-points/diff/2025..2026",
"version": "2025.20260925..2026.20260925",
"content_hash": "sha256:…",
"family": "dc://ssa/bend-points",
"from": { "uri": "dc://ssa/bend-points/2025", "rule_year": 2025, "version": "2025.20260925", "content_hash": "sha256:…" },
"to": { "uri": "dc://ssa/bend-points/2026", "rule_year": 2026, "version": "2026.20260925", "content_hash": "sha256:…" },
"path_format": "RFC 6901 JSON Pointer into the rule-year resource body",
"excluded_fields": ["uri", "rule_year", "version", "content_hash", "published", "supersedes"],
"change_count": 3,
"changes": [
{ "path": "/bend_points/first", "change": "changed", "old_value": 1226, "new_value": 1286 },
{ "path": "/bend_points/second", "change": "changed", "old_value": 7391, "new_value": 7749 },
{ "path": "/wage_base", "change": "changed", "old_value": 176100, "new_value": 184500 }
],
"notes": "Computed at load from the two served records, never hand-written. …"
}
To cache a resource, keep its content_hash and compare it on the next read. When a new rule year is added, read its supersedes link and the matching diff instead of comparing two full bodies yourself.
View URIs (contract 1.9.0)
The ui:// view templates for MCP Apps hosts are linked from their tools (_meta.ui.resourceUri and _meta["openai/outputTemplate"]) and listed in resources/list at a content-hashed URI, ui://digitalcalculator/<view>-<hash>, where the hash is the first 8 hex digits of the SHA-256 of the view’s HTML. Hosts cache views by URI, so a changed view gets a new URI and is picked up. resources/read answers any hash of a known view, and the fixed URIs used before 1.9.0 (such as ui://digitalcalculator/mortgage-result), with the current view, so a host holding an older tools/list still renders a working view. Read the current URIs from tools/list rather than hard-coding them.
From contract 1.10.0 one view can serve more than one tool: the home-scenario view is linked from both mortgage_monthly_payment and evaluate_affordability and works out which result it was given from the result itself. It recomputes adjusted scenarios with the same calculation module the server runs, draws the previous scenario as a dashed line, shows the debt-to-income ratios in words, and, 1.5 seconds after you stop adjusting, tells the assistant which scenario you settled on (the view says so on screen). The home-scenario view and the rebuilt loan view are not yet served by the hosted endpoint, which links its earlier mortgage and loan views, and every earlier URI keeps resolving.
From contract 1.11.0 the same kind of view serves two more tools, again not yet served by the hosted endpoint. emergency_fund_recommendation, which had no view, gains one: the target, a progress ring of the savings so far against it with the time to the goal in words, and the month-by-month savings balance against the target. project_growth’s view is rebuilt as a stacked area of the money put in and the growth on it, year by year, marking the year growth overtakes the money put in. Both recompute with the server’s own calculation module and share a settled scenario with the assistant the same way. The production endpoint keeps its earlier project_growth view and lists no view for emergency_fund_recommendation.
Get a key (self-serve)
A free key is minted by an unauthenticated POST — no signup, no email, no card. It works on your next request.
curl -X POST https://api.digitalcalculator.info/v1/keys/public
Returns HTTP 201 with the key and its allowance:
{
"key": "dc_pub_…",
"tier": "free",
"dailyLimit": 1000,
"windowLimit": 60,
"claimed": false,
"claimToken": "dc_claim_…"
}
claimToken (contract 2.9.0) is private: keep it, and never publish it with the key. To attach an email to a dc_pub_ key later, send POST /v1/keys/claim with {"key": "dc_pub_…", "email": "…", "claimToken": "dc_claim_…"} and Content-Type: application/json. Because a dc_pub_ key is meant to appear in page source, the key alone does not prove it is yours; the claim token does, and with it you can also change the email later. A key created before 2.9.0 has no claim token and cannot be claimed this way: create a new one, or email us.
Then send it as Authorization: Bearer dc_pub_… on any tool call, on either transport.
The key is returned once
We store only its SHA-256 hash, so the key cannot be recovered, re-sent, or emailed — there is no account and no key list yet. Save it when you receive it; if you lose it, mint another. Minting is capped at 5 keys per IP per UTC day.
dc_pub_ keys are the publishable class: they carry the free allowance and are safe to place in client-side code. dc_bearer_ keys are the secret class issued for paid plans — they unlock the Bearer-tier surface (including generate_report) and must never appear in client-side source. Paid keys are created automatically when a subscription starts and shown on the confirmation page for 24 hours.
Rate limits
Two layers apply to every call. The outer layer is a single global API Gateway throttle of about 100 requests/minute (burst 50), shared by all callers — per-API, not per-caller. No tier reserves throughput against it, so a per-tier allowance is a ceiling on consumption, not a guarantee of rate. The inner layer is a per-caller allowance enforced in the application:
- Anonymous tier: 500 calls per UTC day per IP, plus a short-term ceiling of 30 calls per 5-minute window per IP. As of contract 1.2.0, both ceilings meter
tools/callonly —initialize,server/discover,ping,tools/list,resources/list,resources/readandprompts/list/prompts/getnever count against them, so connecting and exploring the server cannot exhaust your budget before your first computation. Exceeding either ceiling returns HTTP 429 withRetry-AfterandX-RateLimit-Limit / -Remaining / -Resetheaders unchanged; the body iscode: "RATE_LIMIT"(retriable: true) on the REST route, and the JSON-RPC error envelope (code: -32000, throttle facts inerror.data) on/mcp— see Status mapping. Terms:LICENSE-API.md - Calls through Claude’s connectors (contract 2.10.0): Claude calls a connector from Anthropic’s servers, from the outbound range Anthropic publishes (
160.79.104.0/21), so under a per-address rule its users would share a few small allowances. Anonymous calls from that range share one allowance of their own: 150 calls per 5-minute window and 5,000 per UTC day, for the whole range together. Every other address keeps the per-IP ceilings above, and a call with a key is counted against the key wherever it comes from - Free key tier (
dc_pub_, minted self-serve): 1,000 calls per UTC day per key, plus 60 calls per 5-minute window per key. Counted against your key alone rather than shared with everyone behind your IP - Bearer tier: a per-key daily cap plus a per-key fixed-window ceiling (boundary-aligned 5-minute buckets), by plan — authenticate with an
Authorization: Bearer <key>header. The daily budget is counted against your key alone and is not consumed by anonymous traffic. See plans and allowances - One meter per key, both transports: a key's allowance is decremented identically whether the call arrives on
/mcpor/v1/tools/{tool}/calculate. There is no second budget to discover - Successful anonymous responses are unchanged: the anonymous gate adds no headers on a PASS (anonymous-parity invariant). The only externally visible difference is the 429 itself
- Bearer is server-to-server today: the API's CORS policy does not yet allow the
Authorizationheader from browsers, so call the endpoint from a backend, CLI, or MCP client — not browser-sidefetch - Need sustained higher volume, or throughput genuinely reserved for you? Email us about Enterprise
Versioning
| Bump | Trigger | Example |
|---|---|---|
| Major | Tool rename, removed field, breaking envelope change | The contract 1.0.0 release: the pre-submission surface consolidation (granular tools absorbed into the four scenario tools) — the 0.x era permitted breaking changes; from that release forward is what the deprecation contract protects. None of the 1.1.0 through 1.8.0 releases that followed removed or renamed anything, so none triggered this class. The first scheduled one removes the camelCase names the 1.8.0 v2 envelope replaced (the rule). |
| Minor | Additive tool, additive output field, tool tier change | v0.8.x added generate_report (Bearer-tier only); v0.11.x promoted money_flow_map to the anonymous tier; contract 1.4.0 added evaluate_affordability; contract 1.5.0 added the optional target input and the input_required result type; contract 1.8.0 added the v2 envelope’s snake_case twins, *_percent fields and inputs echo beside the existing keys |
| Patch | Bug fixes, metadata corrections | Engine defect fixes |
engineVersion | Math changes only | Per-tool SemVer, independent of the package version |
methodology.version | Calculator's methodology document materially updated | ISO date string |
References
- Quickstart — connect + first call
- Changelog — full release history, including the contract-1.0.0 through 1.5.0 entries
- Versioning & Deprecation Contract — the stability rules this surface is governed by
- FAQ — accuracy, support, change policy
- npm package (stdio shim)
- CHANGELOG / version history (npm)
- LICENSE (MIT — package source, ships in the npm package)
- LICENSE-API.md — hosted endpoint terms (acceptable use, rate limits, no warranty)
Questions or integration help: email admin@markcolabs.com.