Gnomon API
Check what your AI is about to do. Send a decision with the facts it rests on, your rules and the numbers that could change, and Gnomon runs up to a million scenarios and answers go, review or stop, with a confidence and every step of how it got there. It also checks AI rewrites and answers fact by fact.
Private beta. The API is open to a small group of beta partners. Details may change before launch, and breaking changes will only ever come in a new version path. Ask for a beta key or try the playground.
Quick start
Ask a business question with numbers. Gnomon plays it out a million times and answers in plain words.
curl https://gnomon-api.deskwithnova.com/v1/models/job_profit \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"inputs": {"price": 28000, "target_margin": 0.1, "hourly_cost": 65, "overheads": 8200,
"materials": {"low": 6800, "likely": 7400, "high": 9200},
"hours": {"low": 90, "likely": 118, "high": 150}}}'{
"tell_the_person": "Gnomon played this out 1,000,000 times and it worked out in 19,896 of 1,000,000 futures (2.0%).",
"plan": "a quote of $28,000 incl. GST",
"succeeded": 19896,
"success_ratio": 0.0199,
"seconds": 0.068,
"drivers": [{ "label": "hours the job takes", "direction": "high" }, { "label": "materials", "direction": "high" }],
"answer": "A quote of $28,000 incl. GST: works out in 19,896 of 1,000,000 futures ..."
}Only 2 in 100 futures make the margin, and the hours are the main risk. Ask /v1/models/job_profit/best-plan and Gnomon tries prices and returns the lowest one that makes the margin safely enough: $32,200, which works in 931,708 of 1,000,000. To have an AI do this for you, connect it.
Check a decision
POST /v1/decision checks a decision before your AI acts on it. Every layer is optional: send the ones that fit.
| Field | Type | Notes |
|---|---|---|
decision | string | Required. What is about to happen, up to 2,000 characters |
values | object | The decision’s own numbers, for the rules: {"price": 28000, "margin": 0.22} |
claims | array | Facts the decision uses, each with name, value, the source_value from your records, and optional source and tolerance |
rules | array | Your rules as formulas over values: {"name": "Margin at least 25%", "check": "margin >= 0.25"}. level is must (default) or should |
text, sources | string, array | A draft and the records it must agree with: every number, price, date, link and name in the draft must be in the sources |
simulation | object | What-if scenarios, as in Run scenarios |
threshold | number | Confidence needed to go ahead without a person. Default 0.85 |
The answer:
{
"action": "stop",
"confidence": 0.0682,
"reasons": ["1 of 2 facts match their sources.", "0 of 1 rules pass.",
"1,000,000 scenarios in 0.068 s: works out in 36.8%.",
"Main risk: labour_hours running high, and the odds turn at about 113.5."],
"fixes": ["Use 72 for Tiling labour rate (October price list).",
"Change the decision so it meets: Margin at least 25%."],
"how": ["Start at 36.8%: 368,160 of 1,000,000 scenarios worked out (36.8%).",
"-15 points: Tiling labour rate is 65, but October price list says 72.",
"-15 points: Margin at least 25%: fails, margin is 0.22."]
}How the confidence is worked out (version 1, beta): it starts at the share of scenarios that worked out (or 100% with no simulation), then loses 15 points for each wrong fact or failed must rule, 5 for each failed should rule and 10 for each fact in the text that no source backs. go needs the threshold and nothing wrong, stop comes below 50% or when a must rule fails, and anything else is review. Every step is in how. The score is not yet calibrated against real outcomes.
Ready-made models
Most business questions are one of a few shapes, so Gnomon has them ready. Send plain numbers to POST /v1/models/{name} and the model runs a million times, answers in plain words and names every default it assumed. GET /v1/models lists every model and its inputs, with no key needed. See them work on the dashboard.
| Model | The question | Main inputs |
|---|---|---|
hire | Can I afford to hire? | cash_now, monthly_costs, monthly_sales or sales_history, salary_per_year, days_per_week, full_time_from_month |
cash_flow | Will the cash last? | cash_now, monthly_costs, monthly_sales or sales_history, one_off_amount, one_off_month |
price_change | Does a new price make more profit? | current_price, new_price, unit_cost, units_per_month, volume_change |
job_profit | Will this quote make the margin? | price (GST counted), materials and hours as ranges, hourly_cost, overheads, target_margin |
stock_order | How much stock to order? | order_quantity, unit_cost, sell_price, demand as a range, leftover_value |
loan | Does a purchase earn back the loan? | amount, annual_rate, years, monthly_benefit as a range, ramp_months |
{
"inputs": {
"cash_now": 12000, "monthly_costs": 39000, "salary_per_year": 65000,
"days_per_week": 3, "full_time_from_month": 11,
"monthly_sales": [49700, 56200, 54000, 50800, 45400, 41000,
37800, 35600, 36700, 38900, 42100, 46400]
},
"detail": true
}The answer starts with tell_the_person, one sentence with the count for the AI to pass on, then has answer in plain words (“works out in 949,625 of 1,000,000 futures”), succeeded, success_ratio, the outcomes that matter, the main risks in drivers and every default in assumptions. With detail, a histogram and, for monthly models, each month’s range. A range is a number or {low, likely, high}. Give sales_history (6 or more past months) and the model learns the business’s real variation, trend and season; POST /v1/history shows what it learned.
Find the best plan
POST /v1/models/{name}/best-plan tries the choices and returns the plan that is safe enough and does the most good: the lowest price that still makes the margin for a quote, the order size that earns the most for stock. Every option is screened on the same 100,000 futures, so they are compared fairly, and the leading three are run a million times each. Options are ranked by their average (expected) outcome, with min_success guarding the risk.
{
"inputs": { ...the same inputs as the model... },
"choices": {"price": [28000, 30000, 31000, 32200, 34000]},
"goal": "best",
"min_success": 0.9
}Leave out choices and Gnomon tries sensible ones for the model. goal is best or safest. The answer lists every option with its share of futures that work out, and the pick in best. Up to 48 options at once; units are charged for the futures run (16 options are about 4.6 million futures, 19 units, a few seconds).
Memory and receipts
PUT /v1/profile saves a business’s numbers and rules once, so the AI does not send them every time. Models fill any input they are not given from the saved values, and decision checks add the saved rules. Only named numbers, lists of numbers and rules are kept, never text, and each key sees only its own.
{
"values": {"cash_now": 12000, "monthly_costs": 39000},
"rules": [{"name": "Margin at least 25%", "check": "margin >= 0.25"}],
"forget": []
}GET /v1/profile shows what is saved and DELETE /v1/profile removes it all. In the connector the same are the remember and recall tools, which need a key.
Add "receipt": true to a model, best-plan or decision call to get a signed receipt: the inputs, the result and the seed, for the business’s records. Gnomon keeps nothing. Anyone can run the same check again for the same numbers, and POST /v1/receipts/verify (no key needed) confirms a receipt is unchanged.
Run scenarios
POST /v1/simulate plays out a plan up to a million times with the uncertain numbers changed each time, and counts how often it works out. A million scenarios take well under a second.
{
"variables": {
"materials": {"dist": "triangular", "low": 6800, "mode": 7400, "high": 9200},
"labour_hours": {"dist": "normal", "mean": 118, "sd": 14, "min": 80},
"rain": {"dist": "chance", "p": 0.3},
"price": 31000, "rate": 72, "overheads": 8200
},
"outcomes": {"cost": "materials + labour_hours * rate + rain * 900 + overheads",
"profit": "price / 1.15 - cost"},
"success": "profit > 0",
"runs": 1000000
}| Part | Notes |
|---|---|
variables | A number, or a range: uniform (low, high), normal (mean, sd, optional min and max), triangular (low, mode, high), lognormal (mean, sd) or chance (p) |
outcomes | Formulas worked out in order: + − × ÷, powers, comparisons, and/or, if-else, min, max, abs, round and a few more. Formulas are read, never run as code |
success | A formula that is true when a scenario works out, such as profit > 0 |
answer | succeeded and success_ratio (963,609 of 1,000,000 is 0.9636), each outcome’s range (p10, median, p90), and drivers: what the failures depend on and where the odds turn |
The same inputs always give the same answer. Scenarios show how likely an outcome is, not a promise.
Connect your AI
Gnomon is also an MCP connector, the standard way AI apps use outside tools. Connected, your AI can call run_model, find_best_plan, check_decision, simulate, learn_from_history, remember, recall, check_text and repair_text by itself.
| Where | How |
|---|---|
| Claude or ChatGPT app | Add a custom connector with https://gnomon-api.deskwithnova.com/mcp/demo and no sign-in. The demo allows 20 calls a minute |
| Claude Code | claude mcp add --transport http gnomon https://gnomon-api.deskwithnova.com/mcp --header "Authorization: Bearer $NOVA_API_KEY" |
| Claude API | MCP connector (beta header mcp-client-2025-11-20): mcp_servers with url …/mcp and your key as authorization_token |
| OpenAI Responses API | A tool of type mcp with server_url …/mcp and an Authorization header |
| xAI (Grok) Responses API | A tool of type mcp with server_url …/mcp and your key in authorization |
{"type": "mcp", "server_label": "gnomon",
"server_url": "https://gnomon-api.deskwithnova.com/mcp",
"headers": {"Authorization": "Bearer $NOVA_API_KEY"}}Signed-in connectors for the Claude and ChatGPT apps, with your own facts and rules, are planned for the paid plans. Until then the apps use the demo address.
Use it without being asked. Gnomon’s tools already tell the AI to call them for decisions with money or risk in them, but each app decides when to use a tool. To make it a rule, add this to the AI’s settings (Claude: Settings, General, personal preferences; ChatGPT: Personalization, custom instructions):
When I ask about, or you are about to recommend, any business decision with money or risk in it (cash flow, prices, quotes, stock orders, loans, payments, plans), check it with the Gnomon connector first without my asking, and tell me how many of the 1,000,000 futures worked out.Code and design
Gnomon also checks the work of AI coding tools like Claude Code. This part runs on your own computer, because it runs your code; nothing is sent anywhere. Install it once, then add it to Claude Code in each project:
pip install -e <path to the nova-gnomon repository> # beta partners get access
claude mcp add gnomon-local -- gnomon-local| Tool | What it does |
|---|---|
compare_code | Runs the version in git and the edited version of a function on the same generated inputs (up to 1,000,000, leaning on edge cases) and lists every input where they differ |
best_code | Runs 2 to 8 versions on the same inputs and keeps the fastest one that is always right |
stress_code | Calls a function many times: errors by kind, slow calls, and memory that keeps growing |
best_design_tokens | Searches 1,000,000 combinations of colours, type and spacing for the ones that pass WCAG contrast and readability rules, keeping your brand, and returns CSS variables |
check_layout | Renders a page in Chrome across screen widths, text sizes and longer text, and finds sideways scrolling, cut-off or overlapping text, small tap targets and low contrast |
Automatic checks. A Claude Code Stop hook runs Gnomon whenever Claude finishes a turn: every typed Python function changed since the last commit runs against its git version on 100,000 inputs, and a change in behaviour stops Claude until it explains or fixes it. Functions that may reach networks, files, databases, email or payments are listed, never run, and the code runs with keys and passwords removed from its environment.
{
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "command": "python",
"args": ["-m", "gnomon_local.hook"], "timeout": 300 }] }]
}
}Generated inputs find many breakages, but agreeing on 100,000 of them is strong evidence, not proof. Functions need type hints, or a short description of their inputs.
Keys and base URL
| Base URL | https://gnomon-api.deskwithnova.com/v1 |
| Auth header | Authorization: Bearer <key> |
| Test keys | nova_test_... real checks, never charged, 100 calls a day |
| Live keys | nova_live_... counted against your balance |
| Format | HTTPS only, JSON in and out, UTF-8 |
| Versions | The major version is in the path. Each response names the model that ran in model (today NOVA v1); send model to pin it |
| Spec file | https://gnomon-api.deskwithnova.com/openapi.json describes every endpoint in the standard OpenAPI format, for tools such as Postman and code generators |
A key is shown once, when it is created. NOVA stores only a hash of it. Keep keys on your server, never in a browser or app.
Text: check an edit
Gnomon also checks rewrites of text: when your AI rewrites an email or a reply, it lists every fact that changed.
POST /v1/check compares an original text with an edited version. Every request field except the two texts is optional.
| Field | Type | Notes |
|---|---|---|
original | string | Required. 1 to 20,000 characters |
edited | string | Required. Up to 20,000 characters |
checks | array | Default all: numbers money dates units names links negation meaning comparisons conditions lists quotes diacritics placeholders code protected_terms |
mode | string | standard (default) accepts the same value written differently, “5” and “five”. strict also reports changes of form, as low |
protected_terms | array | Up to 200 terms, such as product names or codes, that must appear unchanged |
locale | string | Default en-US. Reads dates like 03/11/2026: en-NZ gives 3 November |
offset_unit | string | utf16 (default) matches JavaScript string indexes, codepoint matches Python |
metadata | object | Up to 16 string keys and values, echoed back |
rules | object | Your own wording rules. See Your own rules |
The response:
| Field | Notes |
|---|---|
verdict | pass, review (medium findings only) or fail (any high finding) |
facts_kept | true when there are no high findings |
risk | The highest finding risk, 0 to 100, or 0 when nothing changed |
findings | Each has type, change (changed, removed or added), severity, category, risk, original and edited spans (text, start, end, end exclusive) and a plain message you can show people |
summary | Count of findings by severity |
fix_prompt | Numbered instructions for the AI that made the edit, or null when there is nothing to fix |
usage | characters and billed units: one unit per 1,000 characters across both texts, at least one |
id, model, latency_ms, metadata | For support, versioning and your own records |
Finding types
One rule sets severity: a changed or lost value is high, a weakened promise or a dropped condition is medium, a change of form only is low. All of these run as rules, without an AI model, in milliseconds.
| Type | Example | Severity |
|---|---|---|
money | $1,200 to $1,500, or NZD to USD | high |
number | 3 rooms to 4 rooms, a changed phone number | high |
percent | 5% to 50% | high |
date | 3 November to 13 November | high |
time | 9 am to 9 pm | high |
day | Monday to Tuesday, next week to this week. A day word dropped with nothing in its place is medium | high |
frequency | every month to every year, $8.99/mo to $8.99/yr. Dropped is medium | high |
unit | 5 kg to 5 lb, 14 days to 15 days (14 days to 2 weeks passes) | high |
name | Aroha to Arohanui. A dropped name is medium | high |
email | help@ to support@ | high |
url | A changed or removed link | high |
negation | can cancel to cannot cancel; does not include to includes | high |
protected_term | A listed term missing or altered | high |
meaning | will to may, required to may be needed. A swapped state, closed to open or mandatory to optional, is high | medium |
comparison | over $50 to under $50, before to after, rose to fell. A bound dropped next to an amount, at least 10 hours to 10 hours, is medium | high |
condition | A dropped only, except, unless, per night, each or subject to. plus GST to including GST is high | medium |
list_item | A bullet, numbered step or item of an inline list is gone | medium |
quote | Quoted words changed. A quote no longer there word for word is medium | high |
diacritic | whānau to whanau, Māori to Maori, café to cafe | medium |
placeholder | {first_name}, {{ order_id }}, %s or [CUSTOMER NAME] filled in, dropped or invented | high |
code | Code in backticks or a fenced block changed. Gone is medium | high |
rule | One of your own rules broken | high or medium |
format | 5 to five, strict mode only | low |
Facts that only moved, or are written differently with the same value, are not reported in standard mode. A fact found only in the edit is reported as added: an invented price is as risky as a changed one.
Placeholders, code, link targets such as [pricing](/pricing), and HTML tags are checked whole and kept out of the other checks, so the digits in {item1} are not a number and “if not ready” inside code is not a negation.
Your own rules
Add rules to a check to hold the edited text to your own wording. Every part is optional.
{
"original": "...",
"edited": "...",
"rules": {
"banned": ["risk-free"],
"required": ["Terms and conditions apply."],
"packs": ["nz_fair_trading"],
"style": { "no_em_dash": true, "spelling": "en-NZ" }
}
}| Rule | Finds | Severity |
|---|---|---|
banned | Up to 100 phrases that must not appear in the edited text | high |
required | Up to 100 phrases that must appear, word for word | high |
packs | nz_fair_trading: guarantees, best price, 100%, free, plus GST, false urgency. health: cures, clinically proven, no side effects. finance: guaranteed returns, risk-free, double your money. Reported only when the edit added them | medium |
style | no_em_dash, and spelling (en-NZ, en-AU or en-GB) to flag American spellings the edit added | medium |
Packs point at claims worth a second look. They are not legal advice.
Risk and the AI message
Every finding has a category and a risk from 0 to 100: its severity plus how much harm that kind of change can do. The words around a finding decide safety and legal, so a changed number next to “tablets” ranks above a changed price, and a changed price above a dropped “today”.
| Category | Includes |
|---|---|
safety | Any change in a sentence about doses, medicines, allergens or warnings |
money | Amounts and percentages |
legal | Meaning, dates and numbers in a sentence about refunds, contracts, terms, notice or insurance |
template | Placeholders and code |
compliance | Your own rules |
contact, meaning, dates, numbers, names, format | Everything else, by what changed |
fix_prompt turns the findings into numbered instructions, most risky first. Send it to the AI that wrote the edit, then check the new version again.
Revise your last text. Keep everything else exactly as it is, and make only these changes:
1. Change "Sarah" back to "{first_name}".
2. Put back "{order_id}" from the original.
3. Change "Tuesday" back to "Monday".
4. Put back "except" where it was.Text: repair an edit
POST /v1/repair takes the same body as /v1/check and puts the original words back wherever the edit replaced them: amounts, dates, days, names, links, quotes, placeholders, comparisons, promises, tax wording and accents. The rest of the edit is kept, and the repaired text is checked again.
{
"text": "Your refund of $1,200 will arrive by 3 November.",
"verdict": "pass",
"verdict_before": "fail",
"applied": [
{ "type": "money", "edited": { "text": "$1,500", "start": 15, "end": 21 },
"replacement": "$1,200" }
],
"unresolved": [],
"fix_prompt": null
}A dropped “not”, list item or fact has no safe place to go back to, so it stays in unresolved with a fix_prompt for your AI. When repair cannot be sure where words belong, it leaves them alone.
Text: check an answer
POST /v1/check/answer checks that an answer really answers the question, and that it is backed by the sources it was based on. Send at least one of sources or reference, or leave the AI judge on.
| Field | Type | Notes |
|---|---|---|
question | string | Required. Up to 2,000 characters |
answer | string | Required. Up to 20,000 characters |
sources | array | Up to 20 texts, 100,000 characters in total: what the answer should be based on |
reference | string | A known correct answer |
judge | boolean | Default true: also run the AI judge. The judge is not switched on yet in the beta, so for now the rules run and the response says so in note |
| Finding | Layer | Severity |
|---|---|---|
unsupported_fact | Rules: a number, price, date, unit, link or email address no source gives (high), or a name neither the question nor the sources mention (medium) | high |
wrong_fact | Rules: differs from the reference answer | high |
missing_fact | Rules: the reference gives it, the answer does not | medium |
not_answered | AI judge: the answer is about something else | high |
partly_answered | AI judge: part of the question is left out, and which part | medium |
contradicted | AI judge: a source says otherwise | high |
unsupported_claim | AI judge: no source makes this claim | medium |
The response adds answers_question (yes, partly, no), reason, judged and confidence. If the judge cannot run, the rules still do, and the verdict is at most review: Gnomon never claims a clean pass it did not check. The judge reads your texts as data and never follows instructions written inside them.
curl https://gnomon-api.deskwithnova.com/v1/check/answer \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "How long do refunds take?",
"answer": "Refunds are paid within 10 business days.",
"sources": ["Refunds are paid within 5 business days."]}'Batch checks
POST /v1/check/batch runs up to 50 checks in one call. defaults takes any field except the texts; fields in an item override it. Results come back in the same order, and one bad item returns its own error without failing the batch. Items that error are not charged.
{
"defaults": { "locale": "en-NZ" },
"items": [
{ "original": "...", "edited": "...", "metadata": { "ticket": "4812" } },
{ "original": "...", "edited": "...", "protected_terms": ["SKU-99"] }
]
}Errors
Every error has one shape, is never charged, and never contains your text:
{ "error": { "code": "rate_limited", "message": "Too many requests. Retry after 2 seconds.",
"retryable": true, "request_id": "req_..." } }| Code | HTTP | Retry | When |
|---|---|---|---|
invalid_request | 400 | No | A missing field, a wrong type, nothing to check an answer against |
invalid_key | 401 | No | The key is missing, wrong or revoked |
idempotency_conflict | 409 | No | The same Idempotency-Key with a different body |
too_large | 413 | No | A text over its limit, or more than 50 batch items |
unsupported_language | 422 | No | Text mostly in a non-Latin script (the beta checks English) |
rate_limited | 429 | Yes | Over your key’s limit. Wait for Retry-After seconds |
internal_error | 500 | Yes | A fault on our side |
unavailable | 503 | Yes | Overloaded or in maintenance |
Limits and retries
Beta keys get 60 requests a minute and 5 at once. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Ask us if you need more.
Send an Idempotency-Key header (up to 64 characters) to make retries safe: the same key and body within 24 hours returns the first result and is not charged again. Retry only when retryable is true, with backoff from 1 second, at most 3 times.
Usage
GET /v1/usage returns the calls and units for your key today and this month (UTC), and your limits. It is free and not rate limited, so a dashboard can read it.
{
"key_id": "key_1a2b3c4d5e6f",
"mode": "live",
"today": { "date": "2026-10-24", "calls": 212, "units": 240 },
"this_month": { "month": "2026-10", "calls": 3904, "units": 4410 },
"limits": { "per_minute": 60, "concurrency": 5 }
}During the beta, counts reset when the API restarts. Saved usage arrives with billing.
Privacy
Gnomon keeps no text by default. Texts are checked in memory and dropped when the response is sent. Each call keeps only its id, time, key, size, verdict, finding counts and timing.
Your texts are never used to train NOVA models unless you agree to that in writing. During the beta the AI judge of the answer check will run on OpenAI with storage turned off, once it is switched on; it is off today. The rules never leave NOVA.
Code examples
import os, requests
def check(original: str, edited: str) -> dict:
r = requests.post(
"https://gnomon-api.deskwithnova.com/v1/check",
headers={"Authorization": f"Bearer {os.environ['NOVA_API_KEY']}"},
json={"original": original, "edited": edited, "locale": "en-NZ"},
timeout=10,
)
r.raise_for_status()
return r.json()
result = check(customer_reply, llm_rewrite)
send(customer_reply if result["verdict"] == "fail" else llm_rewrite)const res = await fetch("https://gnomon-api.deskwithnova.com/v1/check", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ original: draft, edited: rewrite }),
});
const { verdict, findings } = await res.json();
for (const f of findings) {
if (f.edited) editor.underline(f.edited.start, f.edited.end, f.message);
}Ready to try it in your product? Ask for a beta key. Tell us what your AI writes and roughly how many checks a month you expect.