Skip to content

Integrate in 15 minutes

Three integration paths, each a handful of calls. All examples run against Monad testnet with the built-in addresses.

pnpm add @ibxlab/mandate viem

1. Your users issue mandates (agent platform, wallet)

You already have a principal: a passkey your app created, an embedded wallet, or a smart account that implements ERC-1271. Build the mandate from your form, have the principal sign it, send it.

import { createMandateClient } from "@ibxlab/mandate";
import { monadTestnet } from "viem/chains";

const client = createMandateClient({ chain: monadTestnet, rpcUrl, signer: relayerAccount }); // signer pays gas for principal-side txs

// Passkey principal (browser): Face ID / Touch ID. rpId must equal your hostname.
const principal = (await client.passkey.load()) ?? (await client.passkey.create({ rpId: location.hostname }));

const draft = client.mandate.build({
  agentId: 1831n,                                   // the agent's ERC-8004 id
  agentKey: "0xAgentExecutingKey",
  targets: [{ address: venue, selectors: ["buy(address,uint256)"] }],
  asset: token,
  spendCap: parseEther("300"), perBlockCap: parseEther("150"),
  maxDrawdownBps: 1500,
  validUntil: new Date(Date.now() + 24 * 3600_000),
});
const signed = await client.mandate.sign(draft, principal);   // one passkey prompt
const tx = await client.mandate.grant(signed);              // signed.hash is the mandate hash

// later
await client.mandate.revoke(signed.hash, principal);         // effective in the block it lands

Principals without a passkey device: deploy a SignerAccount for any secp256k1 owner (an embedded wallet) with client.passkey.deploySignerAccount(owner) and bind it with attachSigner; granting then signs EIP-712 through whatever holds that key. The Privy integration does exactly this with a scoped session signer.

Optional, with @ibxlab/mandate/prf: encrypt the agent's strategy to the passkey and commit it in the mandate.

import { encryptPolicy } from "@ibxlab/mandate/prf";
const nonce = await client.publicClient.readContract({ address: client.addresses.registry, abi: MandateRegistryAbi, functionName: "nonces", args: [principal.address] });
const { vault, policyHash } = await encryptPolicy({ rpId, credentialId: principal.credentialId, principal: principal.address, nonce, policy: strategy });
const draft = client.mandate.build({ ...terms, policyHash }); // store `vault` anywhere; it is ciphertext

2. Your agent executes under a mandate (agent developer)

The agent holds the executing key and the mandate hash. Everything else is read from the chain.

const client = createMandateClient({ chain: monadTestnet, rpcUrl });
const agent = client.agent.load({ mandateHash, executor: privateKeyToAccount(AGENT_KEY) });

const m = await agent.mandate();        // targets, selectors, caps, expiry: what the principal signed
const s = await agent.state();          // spent, remaining, breaker phase, active

try {
  await agent.execute({ target: venue, data: encodeFunctionData({ abi, functionName: "buy", args: [token, amount] }), amount });
} catch (e) {
  if (e instanceof MandateError) {
    // e.name is the Solidity error: TargetNotAllowed, SpendCapExceeded, PerBlockCapExceeded, Tripped, MandateRevoked, MandateExpired…
    // Nothing was sent. Adjust and retry, or stop.
  }
}

amount is the most asset the call may move out of the principal's account. The executor measures the real outflow and reverts with SpendExceedsDeclared if the venue took more. agent.validate(params) runs the same checks without sending, which is what an LLM tool should call first.

LLM agents. sdk/examples/agent-tool.ts turns a loaded agent into a single tool, execute_under_mandate, whose schema is generated from the mandate (allowed selectors, caps, expiry) and whose handler returns the protocol's typed refusal as the tool result. Drop the definition into any tool-calling loop.

3. You consume reputation (marketplace, router, another agent)

const rep = await client.reputation.get(agentId);
// { score: 60, trips: 1, executed: 8, pnlBps: -1510n, attestations: 2, erc8004: { count, value } }
const history = await client.reputation.history(agentId); // every attestation with windows and evidence hashes

Scores are written only by the Chainlink CRE workflow through CREAttestationReceiver → ERC8004ReputationAdapter, and mirrored into the ERC-8004 Reputation Registry with tag mandate-compliance, so they are also readable with plain ERC-8004 tooling. Each attestation's evidenceHash is recomputable from public inputs; the workflow logs the evidence and cre/scripts/verify-evidence.ts checks it.

For dashboards, the Envio schema exposes Mandate, Execution, BreakerEvent and Attestation entities; see Indexer.

Checklist before production

  • Pin @ibxlab/mandate and viem exactly; both ship provenance attestations.
  • Never whitelist the protocol's own contracts as targets (the SDK refuses; do the same if you build mandates by hand).
  • Size venue allowances to the mandate; see Security → Scope notes.
  • Keep the agent key funded with gas only; it never holds principal funds.
  • Open an integration issue to get a review and a listing.