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.

Request: will a $28,000 quote make a 10% margin?
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}}}'
Response, shortened
{
  "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.

FieldTypeNotes
decisionstringRequired. What is about to happen, up to 2,000 characters
valuesobjectThe decision’s own numbers, for the rules: {"price": 28000, "margin": 0.22}
claimsarrayFacts the decision uses, each with name, value, the source_value from your records, and optional source and tolerance
rulesarrayYour rules as formulas over values: {"name": "Margin at least 25%", "check": "margin >= 0.25"}. level is must (default) or should
text, sourcesstring, arrayA draft and the records it must agree with: every number, price, date, link and name in the draft must be in the sources
simulationobjectWhat-if scenarios, as in Run scenarios
thresholdnumberConfidence needed to go ahead without a person. Default 0.85

The answer:

Response, shortened
{
  "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.

ModelThe questionMain inputs
hireCan I afford to hire?cash_now, monthly_costs, monthly_sales or sales_history, salary_per_year, days_per_week, full_time_from_month
cash_flowWill the cash last?cash_now, monthly_costs, monthly_sales or sales_history, one_off_amount, one_off_month
price_changeDoes a new price make more profit?current_price, new_price, unit_cost, units_per_month, volume_change
job_profitWill this quote make the margin?price (GST counted), materials and hours as ranges, hourly_cost, overheads, target_margin
stock_orderHow much stock to order?order_quantity, unit_cost, sell_price, demand as a range, leftover_value
loanDoes a purchase earn back the loan?amount, annual_rate, years, monthly_benefit as a range, ramp_months
Request: POST /v1/models/hire
{
  "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.

Request
{
  "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.

Request: PUT /v1/profile
{
  "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.

Request
{
  "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
}
PartNotes
variablesA number, or a range: uniform (low, high), normal (mean, sd, optional min and max), triangular (low, mode, high), lognormal (mean, sd) or chance (p)
outcomesFormulas 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
successA formula that is true when a scenario works out, such as profit > 0
answersucceeded 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.

WhereHow
Claude or ChatGPT appAdd a custom connector with https://gnomon-api.deskwithnova.com/mcp/demo and no sign-in. The demo allows 20 calls a minute
Claude Codeclaude mcp add --transport http gnomon https://gnomon-api.deskwithnova.com/mcp --header "Authorization: Bearer $NOVA_API_KEY"
Claude APIMCP connector (beta header mcp-client-2025-11-20): mcp_servers with url …/mcp and your key as authorization_token
OpenAI Responses APIA tool of type mcp with server_url …/mcp and an Authorization header
xAI (Grok) Responses APIA tool of type mcp with server_url …/mcp and your key in authorization
OpenAI or xAI Responses API, the tool entry
{"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):

Standing instruction
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:

Set up
pip install -e <path to the nova-gnomon repository>   # beta partners get access
claude mcp add gnomon-local -- gnomon-local
ToolWhat it does
compare_codeRuns 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_codeRuns 2 to 8 versions on the same inputs and keeps the fastest one that is always right
stress_codeCalls a function many times: errors by kind, slow calls, and memory that keeps growing
best_design_tokensSearches 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_layoutRenders 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.

.claude/settings.local.json
{
  "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 URLhttps://gnomon-api.deskwithnova.com/v1
Auth headerAuthorization: Bearer <key>
Test keysnova_test_... real checks, never charged, 100 calls a day
Live keysnova_live_... counted against your balance
FormatHTTPS only, JSON in and out, UTF-8
VersionsThe major version is in the path. Each response names the model that ran in model (today NOVA v1); send model to pin it
Spec filehttps://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.

FieldTypeNotes
originalstringRequired. 1 to 20,000 characters
editedstringRequired. Up to 20,000 characters
checksarrayDefault all: numbers money dates units names links negation meaning comparisons conditions lists quotes diacritics placeholders code protected_terms
modestringstandard (default) accepts the same value written differently, “5” and “five”. strict also reports changes of form, as low
protected_termsarrayUp to 200 terms, such as product names or codes, that must appear unchanged
localestringDefault en-US. Reads dates like 03/11/2026: en-NZ gives 3 November
offset_unitstringutf16 (default) matches JavaScript string indexes, codepoint matches Python
metadataobjectUp to 16 string keys and values, echoed back
rulesobjectYour own wording rules. See Your own rules

The response:

FieldNotes
verdictpass, review (medium findings only) or fail (any high finding)
facts_kepttrue when there are no high findings
riskThe highest finding risk, 0 to 100, or 0 when nothing changed
findingsEach 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
summaryCount of findings by severity
fix_promptNumbered instructions for the AI that made the edit, or null when there is nothing to fix
usagecharacters and billed units: one unit per 1,000 characters across both texts, at least one
id, model, latency_ms, metadataFor 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.

TypeExampleSeverity
money$1,200 to $1,500, or NZD to USDhigh
number3 rooms to 4 rooms, a changed phone numberhigh
percent5% to 50%high
date3 November to 13 Novemberhigh
time9 am to 9 pmhigh
dayMonday to Tuesday, next week to this week. A day word dropped with nothing in its place is mediumhigh
frequencyevery month to every year, $8.99/mo to $8.99/yr. Dropped is mediumhigh
unit5 kg to 5 lb, 14 days to 15 days (14 days to 2 weeks passes)high
nameAroha to Arohanui. A dropped name is mediumhigh
emailhelp@ to support@high
urlA changed or removed linkhigh
negationcan cancel to cannot cancel; does not include to includeshigh
protected_termA listed term missing or alteredhigh
meaningwill to may, required to may be needed. A swapped state, closed to open or mandatory to optional, is highmedium
comparisonover $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 mediumhigh
conditionA dropped only, except, unless, per night, each or subject to. plus GST to including GST is highmedium
list_itemA bullet, numbered step or item of an inline list is gonemedium
quoteQuoted words changed. A quote no longer there word for word is mediumhigh
diacriticwhānau to whanau, Māori to Maori, café to cafemedium
placeholder{first_name}, {{ order_id }}, %s or [CUSTOMER NAME] filled in, dropped or inventedhigh
codeCode in backticks or a fenced block changed. Gone is mediumhigh
ruleOne of your own rules brokenhigh or medium
format5 to five, strict mode onlylow

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.

Request body
{
  "original": "...",
  "edited": "...",
  "rules": {
    "banned": ["risk-free"],
    "required": ["Terms and conditions apply."],
    "packs": ["nz_fair_trading"],
    "style": { "no_em_dash": true, "spelling": "en-NZ" }
  }
}
RuleFindsSeverity
bannedUp to 100 phrases that must not appear in the edited texthigh
requiredUp to 100 phrases that must appear, word for wordhigh
packsnz_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 themmedium
styleno_em_dash, and spelling (en-NZ, en-AU or en-GB) to flag American spellings the edit addedmedium

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”.

CategoryIncludes
safetyAny change in a sentence about doses, medicines, allergens or warnings
moneyAmounts and percentages
legalMeaning, dates and numbers in a sentence about refunds, contracts, terms, notice or insurance
templatePlaceholders and code
complianceYour own rules
contact, meaning, dates, numbers, names, formatEverything 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.

fix_prompt
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.

Response, shortened
{
  "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.

FieldTypeNotes
questionstringRequired. Up to 2,000 characters
answerstringRequired. Up to 20,000 characters
sourcesarrayUp to 20 texts, 100,000 characters in total: what the answer should be based on
referencestringA known correct answer
judgebooleanDefault 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
FindingLayerSeverity
unsupported_factRules: 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_factRules: differs from the reference answerhigh
missing_factRules: the reference gives it, the answer does notmedium
not_answeredAI judge: the answer is about something elsehigh
partly_answeredAI judge: part of the question is left out, and which partmedium
contradictedAI judge: a source says otherwisehigh
unsupported_claimAI judge: no source makes this claimmedium

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.

Request
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.

Request body
{
  "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_..." } }
CodeHTTPRetryWhen
invalid_request400NoA missing field, a wrong type, nothing to check an answer against
invalid_key401NoThe key is missing, wrong or revoked
idempotency_conflict409NoThe same Idempotency-Key with a different body
too_large413NoA text over its limit, or more than 50 batch items
unsupported_language422NoText mostly in a non-Latin script (the beta checks English)
rate_limited429YesOver your key’s limit. Wait for Retry-After seconds
internal_error500YesA fault on our side
unavailable503YesOverloaded 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.

Response
{
  "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

Python
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)
JavaScript
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.