ID MANDAT

Ask before the transaction: does this agent belong to the company, and may it do this?

For shops and agents · 17 October 2026

The problem

An agent presents a request or an approval. Signed requests or a person's approval show which software is knocking or that someone agreed. What often stays open: does the agent belong to the company it names, and what is it permanently allowed or not allowed to do? The occasion is this week's news: Who is the agent? Two answers in one week, and a gap.

The solution

Before accepting, the verifier asks ID MANDAT. One lookup, no account, answer in seconds:

  1. Is the domain registered, and is the agent on its list?
  2. Is the answer authentic (Ed25519 signature) and fresh (15 minutes)?
  3. Is the entry still valid, or revoked, suspended or expired?
  4. Is the planned action listed under “may” (may) or “may not” (mayNot)?

If in doubt: do not proceed.

The lookup: https://idmandat.com/api/v1/verify?domain=example.com&agent=Name. The public keys are at https://idmandat.com/api/v1/keys. To try it first, use the playground with fictional companies.

Example code

Two small examples that perform exactly these four checks. They are examples: please test them yourself and adapt them to your environment before relying on them.

Python (3.9+, pip install cryptography)

"""ID MANDAT: Vor dem Vorgang nachfragen (Beispiel, Python 3.9+).

Braucht:  pip install cryptography
Prüft:    ist der Agent bei der Domain eingetragen, ist die Antwort echt signiert
          und frisch, ist der Eintrag nicht widerrufen/ausgesetzt/abgelaufen,
          und steht die geplante Handlung in "may" bzw. "mayNot".

Wichtig: "may"/"mayNot" sind Angaben des Unternehmens (declared_by_operator).
ID MANDAT bestätigt Identität und Befugnis, nicht Bonität, Vertrauenswürdigkeit
oder Qualität. Im Zweifel gilt: NICHT ausführen (fail closed).
"""
import base64, json, urllib.parse, urllib.request
from datetime import datetime, timezone
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API = "https://idmandat.com/api/v1"


def _b64u(s):
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def _canon(o):
    if isinstance(o, dict):
        return {k: _canon(o[k]) for k in sorted(o)}
    if isinstance(o, list):
        return [_canon(x) for x in o]
    return o


def _get(url):
    with urllib.request.urlopen(url, timeout=10) as r:
        return json.load(r)


def signature_ok(answer):
    """Echtheit prüfen: Ed25519 über die kanonische JSON-Antwort ohne 'signature'."""
    body = dict(answer)
    sig = body.pop("signature", None)
    if not answer.get("signed") or not sig or sig.get("alg") != "Ed25519":
        return False
    keys = {k["key_id"]: k for k in _get(API + "/keys")["keys"]}
    key = keys.get(answer.get("key_id"))
    if not key or key.get("status") != "active":
        return False
    msg = json.dumps(_canon(body), separators=(",", ":"), ensure_ascii=False).encode()
    try:
        Ed25519PublicKey.from_public_bytes(_b64u(key["public_key"])).verify(_b64u(sig["value"]), msg)
        return True
    except Exception:
        return False


def darf_ich(domain, agent_name, handlung):
    """Gibt (erlaubt, grund) zurück. erlaubt ist nur True, wenn alles klar ist."""
    q = urllib.parse.urlencode({"domain": domain, "agent": agent_name})
    try:
        a = _get(API + "/verify?" + q)
    except Exception as e:
        return False, "Abfrage fehlgeschlagen: %s" % e
    if not signature_ok(a):
        return False, "Antwort nicht (gültig) signiert"
    if datetime.fromisoformat(a["expires_at"].replace("Z", "+00:00")) < datetime.now(timezone.utc):
        return False, "Antwort abgelaufen, neu abfragen"
    if not a.get("registered"):
        return False, "nicht registriert (%s)" % a.get("entry_state", "unbekannt")
    if a.get("entry_state") or a.get("revoked"):
        return False, "Eintrag gilt nicht: %s" % a.get("entry_state", "widerrufen")
    treffer = [x for x in a.get("agents", []) if x.get("name") == agent_name]
    if not treffer:
        return False, "Agent steht nicht in der Liste dieser Domain"
    ag = treffer[0]
    if handlung in ag.get("mayNot", []):
        return False, "Handlung ist ausdrücklich ausgeschlossen (mayNot)"
    if handlung not in ag.get("may", []):
        return False, "Handlung ist nicht ausdrücklich erlaubt (may)"
    return True, "erlaubt laut Erklärung des Unternehmens (declared_by_operator)"


if __name__ == "__main__":
    print(darf_ich("idmandat.com", "Grok (xAI)", "draft_social_posts"))
    print(darf_ich("idmandat.com", "Grok (xAI)", "place_order"))

Node (18+, no packages)

// ID MANDAT: Vor dem Vorgang nachfragen (Beispiel, Node 18+, keine Pakete nötig).
// may/mayNot sind Angaben des Unternehmens. Im Zweifel: NICHT ausführen.
import { createPublicKey, verify } from "node:crypto";

const API = "https://idmandat.com/api/v1";
const get = async (u) => { const r = await fetch(u); if (!r.ok) throw new Error(r.status); return r.json(); };
const canon = (o) => Array.isArray(o) ? o.map(canon)
  : (o && typeof o === "object") ? Object.fromEntries(Object.keys(o).sort().map(k => [k, canon(o[k])])) : o;
const b64u = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");

async function signaturOk(a) {
  const { signature, ...body } = a;
  if (!a.signed || !signature || signature.alg !== "Ed25519") return false;
  const key = (await get(API + "/keys")).keys.find(k => k.key_id === a.key_id && k.status === "active");
  if (!key) return false;
  const pub = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: key.public_key }, format: "jwk" });
  return verify(null, Buffer.from(JSON.stringify(canon(body))), pub, b64u(signature.value));
}

export async function darfIch(domain, agent, handlung) {
  let a;
  try { a = await get(`${API}/verify?domain=${encodeURIComponent(domain)}&agent=${encodeURIComponent(agent)}`); }
  catch (e) { return [false, "Abfrage fehlgeschlagen: " + e.message]; }
  if (!(await signaturOk(a))) return [false, "Antwort nicht (gültig) signiert"];
  if (new Date(a.expires_at) < new Date()) return [false, "Antwort abgelaufen, neu abfragen"];
  if (!a.registered) return [false, "nicht registriert (" + (a.entry_state ?? "unbekannt") + ")"];
  if (a.entry_state || a.revoked) return [false, "Eintrag gilt nicht: " + (a.entry_state ?? "widerrufen")];
  const ag = (a.agents ?? []).find(x => x.name === agent);
  if (!ag) return [false, "Agent steht nicht in der Liste dieser Domain"];
  if ((ag.mayNot ?? []).includes(handlung)) return [false, "Handlung ausdrücklich ausgeschlossen (mayNot)"];
  if (!(ag.may ?? []).includes(handlung)) return [false, "Handlung nicht ausdrücklich erlaubt (may)"];
  return [true, "erlaubt laut Erklärung des Unternehmens (declared_by_operator)"];
}

if (import.meta.url === `file://${process.argv[1]}`) {
  console.log(await darfIch("idmandat.com", "Grok (xAI)", "draft_social_posts"));
  console.log(await darfIch("idmandat.com", "Grok (xAI)", "place_order"));
}

The answer is valid for 15 minutes (expires_at); do not cache it longer.

What this does not do

For shops · Check a domain · Technical standard · Register your company

Status: 17 October 2026.