Mandact — The Trust Layer of the Agent Economy

Developers

From zero to your first verification in under ten minutes.

Three calls, no account: issue a sandbox mandate, create a presentation, verify. Every answer carries a signed receipt and exactly one primary code — deterministic and machine-readable.

Playground — real calls

The whole lifecycle in five clicks.

Antworten erscheinen hier — echte Calls gegen /api/v1/*.

Quickstart · cURL

# 1 · Issue a sandbox mandate (no sign-up)
curl -X POST https://mandact.com/api/v1/sandbox/mandates \
  -H "content-type: application/json" \
  -d '{"scope":["purchase.goods"],"amount_limit_per_action":500}'

# 2 · Create a presentation (sandbox convenience; in production the agent wallet does this)
curl -X POST https://mandact.com/api/v1/sandbox/presentations \
  -H "content-type: application/json" \
  -d '{"mandate_id":"mnd_sbx_…","agent_secret":"…"}'

# 3 · Verify
curl -X POST https://mandact.com/api/v1/verify \
  -H "content-type: application/json" \
  -d '{"presentation":{…},"action":{"type":"purchase.goods","amount":{"value":120,"currency":"CHF"}}}'

Quickstart · TypeScript SDK

import { Mandact } from "@mandact/sdk";

const mandact = new Mandact(process.env.MANDACT_KEY);

// Sandbox round trip
const mandate = await mandact.sandbox.issueMandate({
  scope: ["purchase.goods"],
  amountLimitPerAction: 500,
});

const decision = await mandact.verify({
  presentation,
  action: { type: "purchase.goods", amount: { value: 120, currency: "CHF" } },
});

// Exactly one primary code per answer — deterministic, machine-readable.
if (decision.result !== "allow") console.log(decision.primary_code);

Recommended · wrap your tools

The check you cannot forget.

With a direct call you have to place the check in the right spot and build the intent hash yourself. Both get forgotten — and that is exactly where the gaps appear. guard() wraps your function: the hash comes from the arguments it runs with anyway, and the real function never starts before the answer is in.

import { guard, MandactDenied, MandactEscalated } from "@mandact/sdk";

// The real function never starts before the answer is in.
const orderGoods = guard(mandact, "purchase.goods", async (cart) => {
  return await supplier.placeOrder(cart);
});

try {
  await orderGoods(cart);
} catch (e) {
  if (e instanceof MandactEscalated) {
    // Not forbidden — it is waiting for a human. Retry later.
    await createTicket(e.answer.escalation_id);
  } else {
    throw e; // MandactDenied: the action was not covered.
  }
}

MandactDenied and MandactEscalated are deliberately different errors: «drop it» and «wait for a human» call for different reactions. Treating both the same way builds either an infinite loop or gives up too early.

Denial and escalation codes

Stable, versioned, one primary code per answer.

GroupAreaCodes
MD-1xxSignature / format101 invalid_signature · 102 malformed_presentation · 104 stale_pop (PoP older than 60 s)
MD-2xxStatus201 revoked · 202 suspended · 203 expired · 204 consumed · 205 not_yet_valid
MD-3xxScope / constraints301 action_out_of_scope · 302 amount_exceeds_limit · 303 period_limit_exhausted · 304 counterparty_denied
MD-4xxEscalation401 escalation_required — step-up to the principal, timeout 600 s
MD-5xxVerifier policy502 attestation_required

The check order is fixed: signature → status (live, no-store) → time window → proof-of-possession freshness → scope → limits (reserved atomically) → counterparty → verifier policy. Escalations are raised after all checks.

Deutsche Fassung dieser Seite