One answer. Signed, short, checked offline.
Before an agent pays, it asks MIZAN one question: may I pay this party under this rule? The answer is a verdict: ALLOW or REFUSE, the rule it was given under, what the record earned, and a signed token the agent attaches to its payment. The payee, the rail or the contract checks that token with MIZAN's published key, offline, in under a millisecond, without asking MIZAN anything.
1. Ask
The check you already call answers with a verdict: GET /api/check/<subject>. Add payee, amount and currency when the check is for a payment; the verdict is then bound to them. The previous, longer reply, with every check and note, is ?format=legacy on the same URL, and every verdict links it in details. MIZAN SDK releases from before the verdict keep the previous reply until they upgrade.
curl -s "https://studio.mizan.market/api/check/try-me" curl -s "https://studio.mizan.market/api/check/0xabc…?payee=0xabc…&amount=5¤cy=USDC"
The same object everywhere. MCP check(subject, rule) at https://studio.mizan.market/mcp returns it as structuredContent, described by the tool's outputSchema, with a one-line summary and the JSON as text (compat: true gives the previous shape for one version). The A2A check skill returns it as the data part. The SDKs return it with the token on it:
v = mizan.check("0xabc…", payee="0xabc…", amount=5, currency="USDC") # Python
v.decision, v.token, v.allow # allow = ALLOW and the token verified here
const v = await check("0xabc…", { payee: "0xabc…", amount: 5, currency: "USDC" }); // TypeScript, mizan-agent-tools
v.decision, v.token, v.allow
2. The object
A verdict MIZAN issued for the sandbox subject try-me (token shortened):
{
"schema": "mizan.verdict/1",
"decision": "ALLOW",
"subject": "credential:track-btc-trend-4h-link2",
"grade": "PROVEN",
"met": "A passing record in good standing, sealed before its outcomes.",
"measured": {
"metric": "sharpe",
"value": 1.41
},
"rule": {
"id": "mizan.default/1",
"hash": "sha256:2e1025ba1012126cbfd86407a5c88a4932686a66f44ed8df2fc1623f0102d559"
},
"bound_to": {
"payee": "try-me",
"rule": "sha256:2e1025ba1012126cbfd86407a5c88a4932686a66f44ed8df2fc1623f0102d559",
"amount": "5 USDC"
},
"issued": "2026-11-03T22:47:19Z",
"expires": "2026-11-03T23:47:19Z",
"as_of": {
"ledger_size": 1188,
"head": "37b36994bd051b25c8ca4a0f4807980582db5ea9739b274dbc624e79915326e1"
},
"record": {
"id": "credential:track-btc-trend-4h-link2",
"entry": 147,
"verify": "https://studio.mizan.market/api/log/proof/147"
},
"request_id": "req_13c74fdbc79ac7fc85c9",
"token": "mzv1.eyJib3VuZF90byI6eyJ…lA30Hq3UDg",
"kid": "2266fb495f83b0a0",
"status": "https://studio.mizan.market/api/verdict/status/vd_1ae40b86edf7ac36b1b30b2f",
"details": "https://studio.mizan.market/api/check/try-me?format=legacy"
}
A REFUSE for a subject with nothing on record:
{
"schema": "mizan.verdict/1",
"decision": "REFUSE",
"subject": "no-such-agent-zz9",
"reason": "no_record",
"rule": {
"id": "mizan.default/1",
"hash": "sha256:2e1025ba1012126cbfd86407a5c88a4932686a66f44ed8df2fc1623f0102d559"
},
"bound_to": {
"payee": "no-such-agent-zz9",
"rule": "sha256:2e1025ba1012126cbfd86407a5c88a4932686a66f44ed8df2fc1623f0102d559",
"amount": "5 USDC"
},
"issued": "2026-11-03T22:47:19Z",
"expires": "2026-11-03T23:47:19Z",
"request_id": "req_7316a9ec5b66ff83452c",
"token": "mzv1.eyJib3VuZF90byI6eyJ…YF5O-7e9Dg",
"kid": "2266fb495f83b0a0",
"status": "https://studio.mizan.market/api/verdict/status/vd_97dc824911b3b3ac47abe0d7",
"next": {
"proof_request": {
"method": "POST",
"url": "https://studio.mizan.market/api/proof-requests",
"body": {
"subject": "no-such-agent-zz9"
},
"returns": "a private link to forward to the party; when it proves the rule, the next check returns ALLOW"
},
"what_would_pass": [
"a record for this subject that meets the rule; its owner proves it once and the next check returns ALLOW"
]
},
"details": "https://studio.mizan.market/api/check/no-such-agent-zz9?format=legacy"
}
| Field | What it says |
|---|---|
schema | mizan.verdict/1. A new version is a new schema string, never a silent change. |
decision | ALLOW or REFUSE. Nothing else. |
subject | Who was checked, canonical: credential:…, ag:…, evm:0x…, dns:…, web:…; otherwise the subject as given. |
grade | ALLOW only. DECLARED (they say so), RECORDED (logged and timestamped, source named) or PROVEN (checked by math, sealed before its outcomes). Never presented higher than earned. |
met / reason | ALLOW: the bar met, in plain words. REFUSE: one stable reason code from the table below. |
measured | {metric, value, tries, counted_before_outcome}. Only fields the record earned. An unknown field is left out, never written as zero. |
rule | {id, hash}: the rule the answer was given under. The default rule is mizan.default/1; a published rule is req_…@version. |
bound_to | Present when the check was asked for a payment: {payee, rule, amount?}, with amount as <decimal> <CURRENCY>. |
issued, expires | UTC. A verdict is good for one hour by default (ttl 60 s to 24 h). |
as_of | {ledger_size, head}: the ledger state that was read. |
record | The subject's own ledger entry and a link that proves its inclusion. A check is not a ledger row: the check behind a verdict is committed with the other checks of its hour, in one batch, not as its own entry. |
request_id | Also returned as X-Request-Id. Send your own X-Request-Id to have it echoed. |
token, kid | The compact signed verdict and the key that signed it. The token is what you carry. |
status | A live URL: is this verdict still good? Revocation reaches every holder who asks. |
next | REFUSE only: a proof request to forward to the party, and what would pass. |
details | The full answer, with every check and note. Nothing internal is in the verdict itself. |
Fields are present only when true. There are no nulls, no internal notes and no history caveats in a verdict; they live, unchanged, behind details. JSON Schema: mizan-verdict-1.schema.json.
3. Reason codes
A REFUSE carries exactly one code. The codes are fixed for mizan.verdict/1: a code is never renamed or given a new meaning; a change ships as a new version.
| Code | Meaning |
|---|---|
no_record | Nothing is on record for this subject that the rule can read. Not an accusation: unproven is not false. |
below_bar | A record exists, but it does not meet the rule. |
too_few_tries | The record has fewer counted tries than the rule requires. |
expired | Something the answer depends on has expired (a receipt, or the record's standing). |
revoked | The subject's record was revoked. |
superseded | The subject's record was replaced by a later one; check that one. |
rule_unknown | The rule could not be found or understood, so nothing can be allowed under it. |
subject_unresolved | The subject could not be resolved to exactly one party. |
payee_mismatch | The payee named for the payment is not the subject that was checked. |
Rule-specific codes, for a failed part of a rule that none of the codes above describes:
| Code | Meaning |
|---|---|
receipt_invalid | A receipt supplied with the check did not verify or does not belong to the subject. |
integrity_bundle_hash | The record's bundle hash is missing. |
integrity_log_inclusion | The record is not included in the public ledger. |
check_unavailable | The check could not be completed. Do not pay on this answer. |
rule:<check> | Any other failed part of a rule keeps its own stable code, named after the check. Never dropped silently. |
4. The token
token = "mzv1." + b64url(claims) + "." + b64url(signature)
claims = canonical JSON: keys sorted, separators "," and ":",
UTF-8, no whitespace, no floats (amounts are strings)
{typ, iss, kid, jti, decision, subject, grade?, reason?,
rule, rule_id, bound_to?, iat, exp, head, ledger_size}
signature = Ed25519 over the ASCII bytes of "mzv1." + b64url(claims)
b64url = RFC 4648 §5, no padding
kid = sha256(raw 32-byte public key)[:16], listed at
/.well-known/mizan-keys.json
A verifier checks, in order: the prefix; that kid is in the key set it trusts; the signature; that typ is mizan.verdict/1; that exp is in the future; and, when it knows them, that bound_to.payee is the party it is paying and that rule (or rule_id) is the rule it requires. Result codes: valid, malformed, unknown_kid, bad_signature, expired, payee_mismatch, rule_mismatch, revoked, superseded.
Keys. Pin a copy of /.well-known/mizan-keys.json. Keys rotate only by the published ceremony: a new key is appended to that file, old key ids stay listed with a retirement date, and the successor key's hash is published before it is used. A verdict signed by a retired key fails once that key is retired.
Expiry. One hour by default. Offline, a verifier honours exp and nothing else; online, it also reads status.
5. Verify offline
Python (pip install mizan-sdk, stdlib only; with cryptography installed a check takes about 0.1 ms):
import mizan, json
keys = json.load(open("mizan-keys.json")) # your pinned copy of the published key set
r = mizan.verify_token(token, keys, payee="0xabc…", rule="sha256:…")
if r["ok"] and r["claims"]["decision"] == "ALLOW":
release_payment() # no call to MIZAN was made
TypeScript (mizan-agent-tools):
import { verifyToken } from "mizan-agent-tools";
const r = verifyToken(token, { keys, payee: "0xabc…", rule: "sha256:…" });
if (r.ok && r.claims?.decision === "ALLOW") releasePayment(); // offline
// verifyTokenAsync(token, opts) runs on WebCrypto in any runtime
With no keys passed, the Python SDK uses its pinned key and fetches the published key set only when a token names a key it does not know (cached for a day; MIZAN_OFFLINE=1 never fetches).
6. Carry it with an x402 payment
The payer checks the party named in the payment requirement (payTo) with payee set to it, then carries the verdict with the payment in one of two places:
- In the payment requirement it accepted, under the requirement's
extra:extra.mizan = {"token": "mzv1.…", "kid": "…"}. - As a request header beside the payment header:
X-Mizan-Verdict: mzv1.….
{
"scheme": "exact",
"network": "base",
"payTo": "0xabc…",
"maxAmountRequired": "5000000",
"asset": "0x…",
"extra": { "mizan": { "token": "mzv1.eyJ…", "kid": "2266fb495f83b0a0" } }
}
The facilitator or payee checks it offline, in the same step as the payment, with no call to MIZAN: the signature under a key in its pinned copy of the key set; kid in extra is the key that signed; exp is in the future; bound_to.payee is payTo; bound_to.rule is the rule hash it requires; the decision is ALLOW. Anything else: do not settle on it. A reference verifier any facilitator can drop in:
Python (x402-facilitator.py, needs cryptography):
import base64, json, time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def _b64u(s):
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def mizan_verdict_ok(req, keys, rule, token=None, now=None):
"""req: the x402 payment requirement the payer accepted (payTo, extra.mizan); keys: {kid: base64 Ed25519 public key},
your pinned copy of /.well-known/mizan-keys.json; rule: the rule hash you require (sha256:...). Offline. -> (ok, code)"""
m = (req.get("extra") or {}).get("mizan") or {}
t = token or m.get("token") or ""
try:
pre, body, sig = t.split(".")
c = json.loads(_b64u(body))
Ed25519PublicKey.from_public_bytes(base64.b64decode(keys[c["kid"]])).verify(_b64u(sig), f"{pre}.{body}".encode("ascii"))
except Exception:
return False, "bad_token"
norm = lambda s: str(s or "").lower() if str(s or "").lower().startswith("0x") else str(s or "") # noqa: E731
bt = c.get("bound_to") or {}
if pre != "mzv1" or c.get("typ") != "mizan.verdict/1": return False, "malformed"
if m.get("kid") and m["kid"] != c["kid"]: return False, "kid_mismatch"
if not c.get("exp", 0) > (time.time() if now is None else now): return False, "expired"
if not req.get("payTo") or norm(bt.get("payee")) != norm(req.get("payTo")): return False, "payee_mismatch"
if bt.get("rule") != rule: return False, "rule_mismatch"
return (True, "valid") if c.get("decision") == "ALLOW" else (False, "refused")
TypeScript (x402-facilitator.ts, Node, no dependencies):
import { createPublicKey, verify } from "node:crypto";
const b64u = (s: string) => Buffer.from(s, "base64url");
const SPKI = Buffer.from("302a300506032b6570032100", "hex"); // Ed25519 SubjectPublicKeyInfo prefix
/** req: the x402 payment requirement the payer accepted (payTo, extra.mizan); keys: {kid: base64 Ed25519 public key},
* your pinned copy of /.well-known/mizan-keys.json; rule: the rule hash you require (sha256:...). Offline. */
export function mizanVerdictOk(req: any, keys: Record<string, string>, rule: string, token?: string, now = Date.now() / 1000) {
const m = req?.extra?.mizan ?? {}, t: string = token ?? m.token ?? "";
const [pre, body, sig] = t.split(".");
let c: any;
try {
c = JSON.parse(b64u(body).toString("utf8"));
const key = createPublicKey({ key: Buffer.concat([SPKI, Buffer.from(keys[c.kid], "base64")]), format: "der", type: "spki" });
if (!verify(null, Buffer.from(`${pre}.${body}`, "ascii"), key, b64u(sig))) return { ok: false, code: "bad_token" };
} catch { return { ok: false, code: "bad_token" }; }
const norm = (s: unknown) => { const v = String(s ?? ""); return v.toLowerCase().startsWith("0x") ? v.toLowerCase() : v; };
if (pre !== "mzv1" || c.typ !== "mizan.verdict/1") return { ok: false, code: "malformed" };
if (m.kid && m.kid !== c.kid) return { ok: false, code: "kid_mismatch" };
if (!(c.exp > now)) return { ok: false, code: "expired" };
if (!req?.payTo || norm(c.bound_to?.payee) !== norm(req.payTo)) return { ok: false, code: "payee_mismatch" };
if (c.bound_to?.rule !== rule) return { ok: false, code: "rule_mismatch" };
return c.decision === "ALLOW" ? { ok: true, code: "valid" } : { ok: false, code: "refused" };
}
The SDKs carry the same check as mizan.verify_payment_verdict(requirement, keys, rule=…) and verifyPaymentVerdict(requirement, { keys, rule }). On the payer's side, the Python x402 guard attaches a payment-bound verdict on ALLOW (last_decision["x402_extra"]), and accepts a verdict already carried in a 402 only when it is a valid, unexpired ALLOW bound to that payTo.
7. Status and revocation
GET /api/verdict/status/<jti> answers {state, as_of} with state good, revoked, superseded or expired. An ALLOW inherits the live status of the record it rests on: when that record is revoked or replaced, every verdict resting on it says so. GET /api/verdict/revocations lists every unexpired verdict that is no longer good, signed with the same key. A guard reads status when it is online and honours expiry when it is not.
8. Production
- Request id on every reply, in the body and as
X-Request-Id. - Idempotent. The same question within a minute returns the same signed answer. Send
Idempotency-Keyto hold it for the verdict's whole life; a replay carriesIdempotent-Replayed: true. - Rate limits in
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Policy; a 429 carriesRetry-After. - One error shape:
{"error": {"code", "message", "request_id"}}. An error is not a decision: never pay on one.
9. Conformance
vectors.json: valid, expired, wrong payee, wrong rule, tampered, unknown key, revoked status and more, each with the result a verifier must return. They are signed with a test key derived from a public seed; that key is never in MIZAN's key set, so a vector token can never pass against production keys. The Python and TypeScript verifiers pass every vector, and both reference facilitators pass the payment cases built from them: valid, wrong payTo, wrong rule, expired, tampered, wrong kid, and a REFUSE.
details.