Nityaflow receipt format

Version 0.1 (draft) · 25 October 2026

Every Nityaflow check ends in a receipt: a small JSON file that says what was checked, what the independent checker found, who approved it, and what was done. Anyone can confirm a receipt was not changed, and that Nityaflow signed it, using only the public key on this page. Nobody has to trust Nityaflow's server to do it.

What a receipt proves, and what it doesn't

1. The body and its fingerprint

A receipt is a JSON object. Everything in it except fingerprint_sha256 and signature is the body. The fingerprint is the SHA-256 of the body in canonical form, written as 64 lowercase hex characters.

Canonical form: objects have their keys sorted (by plain string order) and are written {"key":value,...} with no spaces; arrays are written [a,b,...]; every other value is written as JSON (strings with JSON escaping, numbers as JSON numbers, null).

function canonical(v) {
  if (Array.isArray(v)) return "[" + v.map(canonical).join(",") + "]";
  if (v && typeof v === "object") return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + canonical(v[k])).join(",") + "}";
  return JSON.stringify(v ?? null);
}

A workflow Receipt step uses one variation: its file is { receipt, items }, and the fingerprint covers { workflow, step, created_at, items }, with item_count equal to the number of items.

Common body fields

FieldMeaning
receipt_version1 for this format.
recipe{ id, name, version } of the check that ran, for example gst-credit-match, upi-settlement or agent-payment-check.
run_id, created_atA random id and the time the receipt was made (ISO 8601, UTC).
inputs, settings, summaryCounts of what was read, the settings used (such as tolerance) and the totals per result group. Invoice or payment lines are not in the receipt; results_sha256 is the fingerprint of the full result lines, so a saved results file can be matched to its receipt.
checker{ passed, checks: [{ name, pass }] }: the independent checker's own findings.
approval{ approved_by, approved_at, statement }, or null when no person has reviewed it yet.
processed_onWhere the files were read (this device, or the Nityaflow server in memory).
test_modetrue when no real money or filing was involved (demo runs).

Payment receipts: mandate, intent and action

An agent-payment-check receipt adds the evidence for who allowed an AI agent to pay, what the person asked for, and what the agent tried to do:

FieldMeaning
mandateThe permission (mandate record v0.1): mandate_version, id, authorised_by, authorised_at, purpose, limits (per_payment, per_period, period, currency, preset, source), valid_from, valid_to, merchants (name, vpa, category), categories, approvals.human_approval_above, never.
intentThe person's own words and what was understood from them: items with quantities and units, budget, shop.
actionWhat the agent proposed: shop, UPI ID, cart lines (name, quantity, unit, price), fees, total.
spent_in_period_beforeWhat was already paid under this mandate in the period, used for the period limit.
checker.checks[]Each check with level (ok, stop or warn) and a plain detail.
paymentoutcome (paid, rejected, stopped), rail, reference, amount. In test mode the rail is "none" and the reference starts with TEST.

Limits in a mandate are always the owner's settings. Presets based on reported NPCI limits (UPI Circle, Reserve Pay) are only starting points: those numbers come from press reports and may change.

2. The signature

Nityaflow signs a short text built from the fingerprint, with Ed25519. The signature object sits next to the fingerprint:

"signature": {
  "v": 2,
  "alg": "Ed25519",
  "kid": "nityaflow-receipts-1",
  "signed_by": "Nityaflow",
  "signed_at": "2026-10-25T16:05:44.764Z",
  "chain": { "id": "463a82f9c8f32d32", "seq": 2, "prev": "a1a1…a1" },
  "value": "<base64url Ed25519 signature>"
}

The signed text is:

v1 (no chain):  nityaflow-receipt:v1:<fingerprint_sha256>:<signed_at>
v2 (chained):   nityaflow-receipt:v2:<fingerprint_sha256>:<signed_at>:<chain.id>:<chain.seq>:<chain.prev>

Signatures without v are v1. Receipts made through the public tool for AI assistants are v1, because they belong to no account.

3. The chain

Each signed-in account has its own chain. Every receipt Nityaflow signs for the account gets the next number, and names the fingerprint of the receipt before it:

Because the chain position is inside the signed text, nobody can reorder receipts, slip one in or quietly drop one without it showing: a missing number is a gap, and a wrong prev is a broken link. Asking Nityaflow to sign the same fingerprint again keeps its first place in the chain. On the Verify page, open several receipt files at once to check the links.

4. The public key

Key id nityaflow-receipts-1 · Algorithm Ed25519

{ "kty": "OKP", "crv": "Ed25519", "x": "2NNm64TPrUlQETtnxm3SvGPwDBiKQ0URxC_aYBSAhEc" }

Also as a file: receipt-key.json. Nityaflow's server gives the same key at …/functions/v1/wf/receipt-key; if the two ever differ, trust neither and write to hello@nityaflow.in.

5. Check a receipt yourself

This runs in a modern browser or in Node 20 and later, with no Nityaflow code:

const KEY = { kty: "OKP", crv: "Ed25519", x: "2NNm64TPrUlQETtnxm3SvGPwDBiKQ0URxC_aYBSAhEc" };
const b64u = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4)), (c) => c.charCodeAt(0));
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");

async function checkReceipt(r) {
  const { fingerprint_sha256: fp, signature: s, ...body } = r;
  const unchanged = hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical(body)))) === fp;
  const text = s.v === 2
    ? `nityaflow-receipt:v2:${fp}:${s.signed_at}:${s.chain.id}:${s.chain.seq}:${s.chain.prev}`
    : `nityaflow-receipt:v1:${fp}:${s.signed_at}`;
  const key = await crypto.subtle.importKey("jwk", KEY, { name: "Ed25519" }, false, ["verify"]);
  const signed = await crypto.subtle.verify({ name: "Ed25519" }, key, b64u(s.value), new TextEncoder().encode(text));
  return { unchanged, signed };
}

Changes to this format

This is a draft. New fields may be added; existing fields keep their meaning. A change that would alter how fingerprints or signatures are made gets a new version letter in the signed text (v3 and so on), and old receipts stay checkable. Suggestions are welcome at hello@nityaflow.in.