About — updated
Normis is a deterministic compliance API for AI agents. Before an agent takes a regulated action, it calls POST /v1/check and receives a decision — allowed, blocked or escalate inside a covered jurisdiction — together with every rule it evaluated, each carrying its own outcome, version and the legal source it cites.
Normis is not a model and does not predict. Interpretation of the law happens offline, in advance: each regulation is read and written down by hand as a structured rule node, and a lawyer signs it. At request time Normis only walks that rule graph in memory. No language model runs in the Normis decision path, which is why the same inputs always produce the same decision, and why the answer comes back in milliseconds rather than at the speed of a model call.
AI agents are being handed real authority in regulated industries — opening accounts, moving money, sharing personal data. AI agents have security layers, rate limits and permission scopes, but no compliance layer. So when an action touches a regulation, the agent does the only thing it can: it guesses, usually by asking a large language model (LLM) what the law probably says.
That fails in two directions. A wrong permit creates a regulatory breach nobody noticed until an audit. A wrong refusal quietly kills legitimate business. And in both cases there is no record of why the agent decided what it decided — which is the part a regulator actually asks for.
The agent describes what it is about to do. Every field the rules need is named up front: each action declares exactly which action_params must be present, and a request missing any of them is rejected as invalid_request rather than answered on partial data.
POST /v1/check
{
"caller": { "agent_id": "agt_01jw3k" },
"action": "onboard",
"jurisdiction": { "primary": "US-NY" },
"subject": { "type": "business", "id": "cus_001" },
"action_params": {
"msb_registered": true,
"kyc_status": "complete",
"beneficial_owners_verified": true,
"bitlicense_number": "NY-VC-0042",
"bitlicense_status": "active"
},
"idempotency_key": "onb-5001"
}
The response carries the decision, every rule that was evaluated, and a machine-readable reason code:
{
"request_id": "nrm_9f2c41e07b8d4a6ab1c35d90e7f2a418",
"decision": "allowed",
"rules": [
{ "rule_id": "us.fed.bsa.msb.registration", "version": "2026-05-06", "source": "31 CFR §1022.380", "decision": "allowed" },
{ "rule_id": "us.fed.bsa.kyc.cdd", "version": "2026-05-06", "source": "31 CFR §1010.230", "decision": "allowed" },
{ "rule_id": "us.ny.bitlicense.required", "version": "2015-06-24", "source": "23 NYCRR §200.3", "decision": "allowed" }
],
"reason_code": "approved",
"reason": "All onboarding requirements met for US-NY jurisdiction",
"escalation": null,
"evaluated_at": "2026-08-03T12:41:09Z"
}
The response lists every rule that was evaluated, not just the one that decided the outcome — each with its own decision, version and the source section it cites. So an allowed answer shows exactly which rules were checked and passed, and a past decision can be read back later and matched to the rule version it was evaluated under. Normis returns the strictest individual decision as the overall one: blocked outranks escalate, which outranks allowed. One prohibition cannot be outvoted by rules that passed.
The full request and response contract, the closed set of reason codes, and the required parameters for every action live in the platform: the playground runs real calls against the live engine, so you read the exact shape of both sides rather than a copy of it.
| Decision | Meaning | What the agent should do |
|---|---|---|
allowed | Every rule in the Normis graph that applies to this action and jurisdiction was satisfied. | Proceed. The cited rules are the record of why. |
blocked | A rule prohibits the action. | Do not proceed. The cited rule explains what would be violated. |
escalate | A rule routes this action to human review — the facts given do not resolve it to a clean yes or no. | Stop and hand off to a person, with the context Normis returns. |
unknown_jurisdiction | The jurisdiction is outside the covered set. No rule was evaluated. | Treat as no answer, not as permission. Normis states where it has no coverage instead of guessing. |
invalid_request | Required action_params for the action were missing. | Fix the call. Normis will not answer on partial data. |
escalate exists because some regulatory judgements cannot be delegated to software. Collapsing them into a yes or a no would be the most dangerous thing a compliance layer could do. An escalation comes back with a human handoff block: what the rule requires, what specifically happened, and what would need to be true to approve. When an operator resolves it, the agent's retry receives that verdict — operator_approved or operator_rejected — with a record naming who resolved it and when.
unknown_jurisdiction is the other half of the same principle. Normis covers a specific, declared set of jurisdictions. Asked about anything else, it says so rather than returning a permissive default.
A compliance answer is only worth something if it is reproducible and attributable. The two available approaches differ on every property that matters:
| Property | Normis rule graph | Asking an LLM at runtime |
|---|---|---|
| Same input, same answer | Always — evaluation is a pure function | Not guaranteed — sampling and model updates change it |
| Legal citation | Every rule evaluated, by rule_id, version and source section | None, or a citation the model may have invented |
| Who is accountable | A lawyer signs each rule node | No signed artifact behind the answer |
| Reading a past decision | Replayable against the rule version it ran under | Depends on a model version you may not be able to reproduce |
| Behaviour outside coverage | unknown_jurisdiction — states it has no answer | Answers anyway, with no signal that coverage is missing |
Three properties follow from putting no LLM in the decision path:
Speed is a side effect rather than the goal: traversing a rule graph in memory is simply much faster than a model call, which is what makes it practical to gate every action instead of sampling a few. Every response reports how long the deterministic evaluation took in a latency_ms field; the design target is under 30 ms.
The pipeline that turns a regulation into a queryable node is the product. Each rule is written as a structured artifact — the jurisdictions and actions it applies to, the conditions that must hold, the decision it produces when they do not, a plain-language statement of the rule, and the legal source it cites. A lawyer reviews and signs it before it ships. Nothing is inferred at runtime.
Conditions are deliberately plain: a flat list of comparisons — field, operator, value — combined with AND, using only ==, !=, <, >, <= and >=. No nesting, no branching, no hidden logic. A rule can be read and checked by a compliance officer who does not write code, which is the whole point of writing it down instead of prompting for it.
The loader refuses to start the service on a malformed rule — a duplicate id, a rule with no legal source, an empty condition list, or a condition testing a field the action does not declare. A broken rule cannot reach production quietly.
Rules are versioned. When a regulation changes, a new version of the rule ships and the superseded revision stays queryable, so a decision made last year can still be read against the rule text it was actually evaluated under. Every decision in the audit log carries the identifier and version of each rule it touched.
Normis is early and deliberately narrow. The current rule set covers three gated actions for fintech in the State of New York (US-NY) — federal BSA/AML requirements plus NY DFS — with rules scoped either to New York specifically or federally across all US jurisdictions. Anything outside that coverage returns unknown_jurisdiction rather than a guess.
| Action | What it gates | Required parameters |
|---|---|---|
onboard | Taking on a new customer — registration, customer diligence and licensing. | msb_registered, kyc_status, beneficial_owners_verified, plus bitlicense_number and bitlicense_status in US-NY |
transfer | Moving funds — counterparty verification, recordkeeping, and review thresholds for large transfers. | amount, currency, counterparty_id, counterparty_verified, transfer_type |
share_pii | Disclosing personal data — consent and data-protection requirements. | consent_token, data_type, recipient_id |
Depth before breadth is the point. A compliance layer that covers one jurisdiction correctly is useful; one that covers fifty approximately is a liability. Coverage compounds from there: the rule graph is shared, not per-customer, so a rule encoded for one customer applies to every customer in that jurisdiction the moment it ships — and a federally scoped rule is written once and matches every covered state added later.
Agents authenticate to POST /v1/check with an API key, issued under API Keys in the dashboard and shown once — Normis stores only a hash, so a lost key is rotated rather than recovered. An idempotency_key on every call replays the stored response on a retry, so a repeated request cannot produce a second, different decision or a duplicate audit entry.
The dashboard is a separate, human-facing surface at app.normis.dev, where you can read every rule in the graph, review escalations waiting on a person, manage API keys, and read the audit log. The playground there is not a simulator: it runs the same engine through the same pipeline, so a run appears in your audit log exactly as an agent's call would.
The audit log keeps more than the API returns. For every rule that fired it records the field tested, the value expected and the value received. It is not a convenience feature: it is the record you draw on when a regulator, an auditor or a counterparty asks what your agents did and on what basis — a decision per call, with the rules it evaluated, the version of each, and the values they were tested against.
Two brothers, Andrey Li and Danil Li, both product engineers — between them fintech, AI/ML, LegalTech and high-frequency trading. The thesis behind Normis is simple: the next bottleneck for autonomous AI is not intelligence, it is permission. Agents will be able to do far more than they are allowed to do, and the gap will be closed by infrastructure that can state, deterministically and with a citation, what is permitted.
Normis is not a law firm and does not provide legal advice. Its decisions and citations are for engineering and informational purposes; they may be incomplete or wrong for a specific situation, and using Normis creates no lawyer–client relationship. You remain responsible for your own regulatory compliance and for the actions your systems take. See the Terms of Service for the full statement.
Questions, or want access? Contact support@normis.dev, or get a key under API Keys in the dashboard.
← Back to normis.dev