approved, denied or refused — the rules it
cited, a replayable derivation and, on approval, a proof bundle you can check yourself. This guide builds
that gate end to end; the Verified Decisions API page is the contract behind it.
Before you start
The examples assume
NSR_API=https://api.nsr.stateset.com and NSR_API_KEY=nsr_your_key are exported in your shell.
Two encodings are easy to mix up. Stored rules (
/api/v1/rules) use head_predicate,
head_args and body atoms keyed by predicate. Request-scoped facts and rules on
/v1/decisions use { "predicate": { "name", "args" } } envelopes and if / then atoms keyed by
name. A string starting with ? is a variable; everything else is a value.1
Lint the policy, then store it
Three Horn-clause rules make the refund policy: a permit with a negation-as-failure guard, a deny,
and a review threshold using the evaluable builtin Then install all three with
gt. Lint each one first — POST /api/v1/rules/lint
takes the same body as a create, answers { "diagnostics": [...], "has_errors": bool } and installs
nothing. RULE007 (head variable unbound in the body) and RULE008 (builtin as head) are the errors to expect.POST /api/v1/rules/batch. Each rule is created independently — a
failure does not roll back the others — and a name that already exists is replaced, not duplicated.Response
The head names also follow the conventions the gate recognises lexically (
may_*, *_blocked,
*_requires_review), so the verdict is the same whether it reads the declared effect or the
head. Deny wins over permit; a fired review rule yields refused with requires_human_review: true.2
Ask for the decision
POST /v1/decisions. The stored policy is pulled in by hydrate_org_context: true (the default);
the live facts about this order travel in the request. Declaring an authorization_goal is what makes
an approval carry a verifiable_bundle; external_ref lets you report the real outcome later by your own order id.mode: "safe", the most thorough verification, because they are accountable outputs.
The Idempotency-Key makes a retry within 24 hours replay the stored response — same decision_id,
not re-decided, not re-billed — with idempotent-replayed: true.Response (approved)
x-nsr-decision: approved,
x-nsr-outcome-billable: true, x-nsr-outcome-cost-micros: 5000), so a proxy can log the outcome
without parsing the body. Keep decision_id and proof.request_policy_hash with the refund:
GET /v1/decisions/{id} returns the record — verdict, cited rule ids, the replayable request
snapshot, the same policy hash and, once reported, the real outcome — while it is inside the
retained window; a 404 means it rotated out of the bounded log, and billing truth lives in metering.3
Read the three verdicts
Change the facts and the verdict follows. Add Make the order $620.00 instead (
fraud_flagged(order_10042, chargeback_history)
and the deny rule fires: "decision": "denied", "cited_rules": ["fraud_blocks_refund"], with a
plain_explanation that names the flag. Deny wins even though the permit rule’s other premises hold.Drop inspection_passed and the engine cannot prove the goal. It does not guess — it refuses and
names the exact premise that would unblock the proof, in the shape you can echo straight back:Response (refused — evidence gap)
"args": ["order_10042", 620]) and the review rule fires — the
other kind of refusal, with no missing_facts, because a review or safety refusal must not
invite resubmission:Response (refused — human review)
4
Verify the proof without trusting the engine
Save the approval’s It is a real check. Delete the
verifiable_bundle exactly as returned and POST it to POST /v1/proofs/verify.
The handler is stateless — no tenant state, no database. The bundle carries its own facts,
rules, proof and org_id, and the checker replays the derivation against nothing but those.Response
inspection_passed triple from facts in bundle.json and send
it again — still a 200, because an unsound proof is a valid answer, not a client error:Response
Since v0.9.4 the checker verifies entailment, not citation shape: every cited fact is unified with
the atom the rule needs, heads and bodies are replayed, builtins are evaluated, and uncertified negation
fails closed. The same kernel ships as the dependency-light
nsr-proof-core crate for offline
verification; upgrade it with the server, since an older verifier accepts proofs the server now rejects.verifiable_bundle is present only when you declared an authorization_goal and the engine
reproduced the proof; it is skipped above 64 facts or 64 rules across request and org scope, and for
facts of arity three or more. The verdict stands on its own derivation either way.5
Wire the gate into the agent, fail closed
The agent may call the refund tool only after an Three rules the code encodes:
approved verdict whose bundle verifies. Every
other path — denied, refused, an HTTP error, a timeout, an unreachable engine — ends without a side effect.- Absence of a verdict is not a verdict. A timeout, a connection error, a
5xx, or the503the engine returns when it cannot complete machine inference all mean do not refund.5xxresponses are never cached under an idempotency key, so a later retry reaches a healthy backend. - Resubmit at most once, only for an evidence-gap refusal, only with facts your own systems
supplied. Never lower
confidence_thresholdto turn a refusal into an approval. - Log
decision_idandrequest_policy_hashnext to the refund, then close the loop from the system that learns the truth, keyed by the order id it already holds:
honored, reversed, overridden or escalated feed GET /v1/decisions/calibration, which
tells you whether a reported 0.94 holds up 94% of the time — and therefore where an auto-approve
threshold can defensibly sit.What you built
Troubleshooting
The decision is refused although I supplied every fact
The decision is refused although I supplied every fact
Check the encoding — facts are
{ "predicate": { "name", "args" } } envelopes — and that the subject
token matches in the facts and in authorization_goal. A threshold needs the raw number
(["order_10042", 84]), not a precomputed boolean. If missing_facts is empty and
requires_human_review is true, a review rule or the GSS safety gate fired; more facts will not change that.The approval has no verifiable_bundle, or verify says false on an untouched one
The approval has no verifiable_bundle, or verify says false on an untouched one
No bundle: you did not declare
authorization_goal, a fact had three or more arguments, or the
combined request-plus-org facts or rules exceeded 64. verified: false: send the bundle
byte-for-byte, with no re-serialisation or wrapper, and make sure an offline nsr-proof-core is at
least v0.9.4.402 on POST /v1/decisions, or 404 on GET /v1/decisions/{id}
402 on POST /v1/decisions, or 404 on GET /v1/decisions/{id}
402 means the organization’s outcome quota is exhausted — refusals count too; watch
x-nsr-outcome-remaining and the billing pages in the NSR console. 404 on a
recent decision means the bounded log rotated it out; report outcomes as they happen and use
GET /v1/decisions/export for a durable JSONL trail.Next steps
Verified Decisions API
The full contract — rule effects, negation, the outcome gate, soundness invariants and the v0.9.4 verification changes.
Agent Gate
The same fail-closed pattern as an MCP gateway, so the agent never holds an unguarded refund tool.
NSR MCP server
nsr_decide, nsr_verify_proof and nsr_record_outcome_by_ref as agent tools.Endpoint reference
Generated pages for
/v1/decisions, /api/v1/rules and /v1/proofs/verify, with playgrounds.