# Neumann > Neumann is an agent-accessible **Monte Carlo simulation service**. Give it a > graph of probabilistic root nodes (each sampling from a distribution) and > calculated nodes (formulas over their inputs); it runs thousands of trials and > returns the resulting distribution as summary statistics. REST + MCP, built for > AI agents and humans. Neumann supports agent-to-agent cold start: any agent can POST to `/api/v1/accounts` to create an account, receive an API key, and immediately run simulations using starter credits — no human in the loop. > **Base URL (important for cold-start agents):** this document was served from > `https://stag-neumann.krobar.ai`. Use that as your base URL. Every path below is host-relative — > prepend `https://stag-neumann.krobar.ai`. An account created on one environment (localhost / > staging / prod) is not valid on another; don't mix them. ## Cold-start quickstart (3 steps) **Step 1 — Create an account.** No request body. Returns `{account_id, api_key, credit_balance, created_at}`. Save the `api_key` — it is shown only once. ``` curl -X POST https://stag-neumann.krobar.ai/api/v1/accounts ``` **Step 2 — Run a simulation.** Two equivalent surfaces: - **MCP** (for MCP-native clients): connect to `https://stag-neumann.krobar.ai/mcp` over the Streamable-HTTP transport, then call the `run_simulation` tool. Authenticate by passing your **raw signup `api_key` as the `api_key` tool argument**. Omit it to run anonymously (free, no credit accounting). The raw `api_key` is NOT a bearer token — do not put it in an `Authorization` header. Only opaque tokens prefixed `nmn_oat_` / `nmn_pat_` are valid `Authorization: Bearer` values; mint one with `POST https://stag-neumann.krobar.ai/api/v1/tokens`. - **REST** (plain HTTP): POST the same arguments to `https://stag-neumann.krobar.ai/api/v1/runs`, or invoke any tool directly at `https://stag-neumann.krobar.ai/mcp/tools/run_simulation`. Send your raw signup key as the `X-API-Key` header (or the `api_key` body field) — again, the raw key is NOT a `Bearer` token. > **POST `/mcp` works without a trailing slash:** a body-bearing > `POST https://stag-neumann.krobar.ai/mcp` is answered with a `308` to `/mcp/`, which preserves the > method and JSON-RPC body, so the call reaches the handler. (Older builds > returned a `307` that naive clients turned into a bodyless GET.) Copy-pasteable example — a tiny business case (`revenue = units_sold * price_per_unit`, two probabilistic root nodes feeding one calc node): ``` curl -X POST https://stag-neumann.krobar.ai/mcp/tools/run_simulation \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d '{ "graph": { "contract_version": "1.0", "root_nodes": [ { "id": "units_sold", "name": "Units sold per month", "distribution_type": "normal", "distribution_params": { "mean": 1000.0, "std": 150.0 } }, { "id": "price_per_unit", "name": "Price per unit (USD)", "distribution_type": "triangle", "distribution_params": { "min_val": 18.0, "mode_val": 25.0, "max_val": 40.0 } } ], "calc_nodes": [ { "id": "revenue", "name": "Monthly revenue (USD)", "formula": "{e_units} * {e_price}" } ], "edges": [ { "id": "e_units", "source": "units_sold", "target": "revenue" }, { "id": "e_price", "source": "price_per_unit", "target": "revenue" } ], "run_params": { "trials": 5000, "time_periods": 1 } }, "seed": 42 }' ``` **Step 3 — Read the result.** A small run returns the summarised `SimulationResult` inline: ```json { "run_id": "…", "status": "completed", "tier": "small", "total_trials": 5000, "credits_debited": 1, "result": { "contract_version": "1.0", "trials": 5000, "time_periods": 1, "summary_statistics": [ { "time_period": 0, "nodes": { "revenue": { "percentiles": {"p10": "…", "p50": "…", "p90": "…"}, "basic_stats": {"mean": "…", "std": "…", "min": "…", "max": "…"} } }, "edges": { "…": "…" } } ] } } ``` Large runs (trials × nodes × periods over the threshold) return a `run_id` with `status: "accepted"` and `result: null` — stream live progress as Server-Sent Events from `POST https://stag-neumann.krobar.ai/api/v1/runs` (set `stream: true`), or poll `get_results(run_id)`. ## Tools - `run_simulation` — Run a Monte Carlo simulation on a supplied graph. Small runs return the full summarised result inline; large runs return a run_id to stream (POST /api/v1/runs SSE) or poll (get_results). Debits credits per run for authenticated callers. - `oat_sensitivity` — Run a one-at-a-time (OAT) tornado sensitivity analysis: for each scalar root node, sweep it over a set of quantiles (others held at baseline) and measure the target node's response. Returns a tornado-ranked list (response_curve, elasticity, rank). Debits one credit per analysis for authenticated callers. - `scenario_analysis` — Best/median/worst scenario summary for a target node across trials. Runs the simulation once (reusing the run pipeline) and returns, per requested percentile (default worst p0 / median p50 / best p100), the target node's output value plus a best-effort per-root driving input. Debits one credit per analysis for authenticated callers. - `validate_graph` — Validate a graph with the engine's ModularGraphValidator and return a structured list of issues (rule, severity, affected elements). - `estimate` — Pre-flight runtime/memory estimate for a graph without running it: estimated peak memory, complexity, the tier it would run as, and the credit cost. - `solve_distribution_parameters` — Solve distribution parameters from a target min/max range and confidence so a human-style estimate becomes engine-ready distribution_params. - `get_results` — Fetch the status and summary for a previously-submitted run. - `feedback` — Send feedback, a feature request, or a bug report to the Neumann team. Emails the submission to the team and CCs your sender_email so you keep a copy. Optionally pass api_key to attach your account id for context; anonymous submissions are allowed. These tools are reachable three ways and cannot drift: as MCP tools at `https://stag-neumann.krobar.ai/mcp`, as REST twins at `https://stag-neumann.krobar.ai/mcp/tools/`, and (for runs) as `POST https://stag-neumann.krobar.ai/api/v1/runs`. List them with `GET https://stag-neumann.krobar.ai/mcp/tools`. ## The graph contract - **root_nodes** — sample from a distribution each period. Valid `distribution_type` values: `bernoulli`, `beta`, `binomial`, `categorical`, `constant`, `constant_array`, `dirichlet`, `discrete_gamma`, `discrete_lognormal`, `discrete_normal`, `exponential`, `gamma`, `log_normal`, `multivariate_normal`, `normal`, `poisson`, `triangle`, `uniform`. See `GET https://stag-neumann.krobar.ai/api/v1/contract` for each distribution's parameters. - **calc_nodes** — computed from incoming edges. `formula` is an expression over incoming edge ids, e.g. `"{e_units} * {e_price}"`; `null` sums all inputs. - **edges** — directed `source` → `target` connections (carry a node's value). - **run_params** — `trials`, `time_periods`, and an optional `output_request`. Pre-flight a graph for free with the `validate_graph` and `estimate` tools before spending credits on a run. Turn a human "between X and Y" estimate into distribution params with `solve_distribution_parameters`. ## Time periods are 1-BASED in formulas (read this — it bites everyone) - Inside ANY per-period formula, `{time_period}` is **1-based**: the first period is `{time_period} == 1`, the second is `2`, and so on. There is no period 0 in a formula. - **Silent-zero trap:** a mask written as `{time_period} == 0` NEVER fires — it evaluates to false (0) for every real period, so the term it gates silently contributes zero the whole run, with no error. To target the first period write `{time_period} == 1`. - The post-simulation **aggregation** series index (`npv`, `irr`, `payback`, …) is by contrast **0-based**: the first period is undiscounted (period 0). Don't mix the two conventions on one series — e.g. discounting a cash flow with `pv(rate, value, {time_period})` (1-based, first period discounted by ^1) AND then requesting an `npv` aggregation (0-based) double-shifts the first period. Worked time-phased example — a 3-period ramp where revenue only starts in period 2 (a launch in the second period): ``` "calc_nodes": [ { "id": "rev", "formula": "{e_base} * if({time_period} >= 2, 1, 0)" } ] ``` Here period 1 yields 0 (pre-launch) and periods 2–3 pass `e_base` through. If you had written `if({time_period} == 0, 0, 1)` to "zero out the first period", it would do nothing — period numbering starts at 1, so the `== 0` test is dead and every period would be multiplied by 1. ## Formula functions & aggregations Functions usable inside a calc-node `formula` (per-period) and post-simulation `aggregation` metrics (series-aware). The full machine-readable list — with each function's `kind`, description, and arity — is at `GET https://stag-neumann.krobar.ai/api/v1/contract` (the `formula_functions` block): - `abs` (formula) *(args: 1)* — Absolute value. - `and` (formula) *(args: 1..N)* — Logical AND of its arguments. - `average` (formula) *(args: 1..N)* — Arithmetic mean of its arguments. - `binomial` (formula) *(args: 2)* — Sample a binomial outcome. - `ceil` (formula) *(args: 1)* — Round up to the nearest integer. - `discount` (formula) *(args: (rate, n))* — Discount factor 1 / (1 + rate) ** n. n is the 1-based {time_period}. Pairs with the 1-based per-period convention; the post-sim 'npv' aggregation is 0-based — don't combine them on one series. - `exp` (formula) *(args: 1)* — e raised to the argument. - `floor` (formula) *(args: 1)* — Round down to the nearest integer. - `if` (formula) *(args: 3)* — Ternary: if(cond, then, else). - `index` (formula) *(args: 2)* — Element at a position in an array argument. - `length` (formula) *(args: 1)* — Number of elements in an array argument. - `ln` (formula) *(args: 1)* — Natural logarithm. - `log` (formula) *(args: 1)* — Base-10 logarithm. - `max` (formula) *(args: 1..N)* — Largest of its arguments. - `mean` (formula) *(args: 1..N)* — Arithmetic mean of its arguments (alias of average). - `min` (formula) *(args: 1..N)* — Smallest of its arguments. - `not` (formula) *(args: 1)* — Logical negation. - `or` (formula) *(args: 1..N)* — Logical OR of its arguments. - `pow` (formula) *(args: 2)* — Raise the first argument to the power of the second. - `product` (formula) *(args: 1..N)* — Product of its arguments. - `pv` (formula) *(args: (rate, value, period))* — Present value of a cash flow discounted by 'rate' over 'period' periods: value / (1 + rate) ** period. period is the 1-based {time_period}; the first period is discounted by ^1. Do NOT also request an aggregation 'npv' on the same series (it is 0-based and would double-shift period 1). - `round` (formula) *(args: 1..2)* — Round to the nearest integer (or to N decimals). - `sqrt` (formula) *(args: 1)* — Square root. - `sum` (formula) *(args: 1..N)* — Sum of its arguments. - `cagr` (aggregation) *(args: series)* — Compound annual growth rate from the first to the last period of the series. - `cumsum` (aggregation) *(args: series)* — Terminal value of the cumulative sum across periods (sum_t series[t]). - `discounted_payback` (aggregation) *(args: series, rate)* — Like payback, but on the rate-discounted cumulative series. Requires a 'rate' param. - `irr` (aggregation) *(args: series)* — Internal rate of return per trial (the per-period rate where NPV is zero; series index 0-based). - `mirr` (aggregation) — Aggregation metric 'mirr'. - `npv` (aggregation) *(args: series, rate)* — Net present value per trial: sum_t series[t] / (1+rate)**t with a 0-BASED series index (period 0 undiscounted). Requires a 'rate' param. Model an up-front outlay as a negative period-0 cash flow. - `payback` (aggregation) *(args: series)* — Number of periods until the cumulative (undiscounted) series turns non-negative. - `prob` (aggregation) — Aggregation metric 'prob'. - `terminal_value` (aggregation) — Aggregation metric 'terminal_value'. ## Credit model - New accounts get starter credits on signup (`POST /api/v1/accounts`). - A **small** run costs 1 credit; a **large** run costs more. `estimate` tells you the tier and exact `credit_cost` before you run. - Anonymous callers run free (no signup), but get no credit accounting. - Out of credits → `402` with `error.code = "insufficient_credits"`. Read your balance and ledger at `GET https://stag-neumann.krobar.ai/api/v1/accounts/me/ledger`. ## Links All paths are host-relative — prepend `https://stag-neumann.krobar.ai`. - [`/agents.md`](https://stag-neumann.krobar.ai/agents.md) — longer, fully self-contained quickstart - [`/.well-known/mcp.json`](https://stag-neumann.krobar.ai/.well-known/mcp.json) — machine-readable MCP descriptor - [`/server.json`](https://stag-neumann.krobar.ai/server.json) — MCP-registry manifest - [`/api/v1/contract`](https://stag-neumann.krobar.ai/api/v1/contract) — distribution + formula-function catalogs - [`/api/v1/tokens`](https://stag-neumann.krobar.ai/api/v1/tokens) — mint an opaque `nmn_oat_`/`nmn_pat_` bearer (POST) - [`/mcp/tools`](https://stag-neumann.krobar.ai/mcp/tools) — list the agent tools (REST) - [`/docs`](https://stag-neumann.krobar.ai/docs) — Swagger UI (interactive) - [`/openapi.json`](https://stag-neumann.krobar.ai/openapi.json) — machine-readable OpenAPI schema