Skip to content

SDK reference

@ibxlab/mandate is a TypeScript SDK on top of viem. Three entry points:

Import Contents
@ibxlab/mandate createMandateClient, typed errors, EIP-712 helpers, ABIs, addresses, policy-vault helpers
@ibxlab/mandate/prf Mera PRF: namespaced salts, encrypted policy vaults, per-agent identities (browser)
@ibxlab/mandate/privy Privy server-wallet integration: agent wallets, mandate-mirroring policies, session signers (server)

All amounts are bigint in asset units. All hashes and addresses are 0x-prefixed hex strings.

createMandateClient(config)

createMandateClient({
  chain?: Chain;                 // default monadTestnet
  rpcUrl?: string;               // default: the chain's public RPC
  transport?: Transport;         // bring your own instead of rpcUrl
  addresses?: Partial<MandateAddresses>;  // override the built-in deployment
  signer?: Signer;               // pays gas for principal-side txs (deploy, grant, revoke); optional for reads
  storage?: PrincipalStorage;    // where passkey.create persists the principal; default localStorage or memory
}): MandateClient

The client exposes publicClient, addresses, and the four modules below. Reads are batched over one multicall-aware transport, so a screen refresh is one RPC round trip.

client.passkey

Call Returns Notes
create({ rpId, rpName?, userName?, software? }) PasskeyPrincipal Creates the passkey (WebAuthn with the PRF extension requested, or a WebCrypto key with software: true), deploys its PasskeyAccount with signer, persists the principal
createKey(opts) PasskeySigner Key only, no transaction. For relayer-deployed accounts: send publicKey to your server, then attach
deployAccount(signer, payer?) Address Deploys a PasskeyAccount for an existing key
attach(signer, address) PasskeyPrincipal Binds a key to an already-deployed account; verifies the onchain public key matches
save(principal) / load() / clear() Persistence through the configured storage
fromJSON(stored) PasskeyPrincipal Rehydrate from StoredPrincipal
deploySignerAccount(owner, payer?) Address Deploys a SignerAccount for a secp256k1 owner (embedded wallet, EOA, ERC-1271 contract)
attachSigner({ address, owner, signTypedData }) SignerPrincipal Binds a deployed SignerAccount to whatever signs EIP-712 for its owner

A Principal is either { kind: "webauthn" \| "software", address, publicKey, credentialId?, signChallenge(hex) } or { kind: "signer", address, owner, signTypedData(td) }.

client.mandate

Call Returns Notes
build(params) MandateDraft Pure. Flattens targets: [{ address, selectors }] into the onchain parallel arrays; refuses the protocol's own contracts as targets
sign(draft, principal) SignedMandate Reads the principal's registry nonce, computes hash and the EIP-712 digest, signs with the principal
grant(signed, { signer? }) TxResult Sends MandateRegistry.grant. signed.hash is the mandate hash
revoke(hash, principal, { signer? }) TxResult Passkey- or EIP-712-authorised revoke through the account; effective in the block it lands
revokeDigest(account, hash) Hex The digest a passkey must sign to revoke, for relayed flows
revokeWithSignature(account, hash, signature) TxResult Relay a pre-signed revoke
get(hash) Mandate The stored mandate
state(hash) MandateState spent, spentThisBlock, remaining, remainingThisBlock, revoked, active, breaker (Armed/Tripped/Cooldown), drawdownBps
validate(hash, target, selector, amount) void Dry run against MandateRegistry.validate; throws the typed error the chain would

MandateParams: agentId, agentKey, targets, asset? (default: the demo asset), spendCap, perBlockCap, maxDrawdownBps (10000 disables the breaker), validAfter?, validUntil (Date or unix seconds), policyHash?.

client.agent

const agent = client.agent.load({ mandateHash, executor: Signer });
agent.hash; agent.address;
await agent.mandate();                 // Mandate (cached)
await agent.state();                   // MandateState
await agent.validate({ target, data, amount });   // throws MandateError, sends nothing
await agent.execute({ target, data, amount });    // validate -> simulate -> send -> wait; returns TxResult

amount is the upper bound on asset outflow the call may cause. The executor measures the real outflow and reverts with SpendExceedsDeclared if it is higher.

client.reputation

Call Returns
get(agentId) { agentId, score, trips, executed, pnlBps, window, evidenceHash, attestations, erc8004: { count, value, decimals } }
history(agentId) Attestation[] oldest first: complianceScore, tripCount, executedCount, realisedPnlBps, windowStart, windowEnd, evidenceHash

Typed errors

MandateError extends Error. name is the Solidity custom error name and args its decoded arguments, so e instanceof MandateError && e.name === "SpendCapExceeded" narrows. Every revert from the registry, executor, breaker, accounts, submitter, adapter and CRE receiver is decoded; decodeMandateError(data) and toMandateError(unknown) are exported for callers that use viem directly.

TargetNotAllowed(target, selector) · SpendCapExceeded(requested, remaining) · PerBlockCapExceeded(requested, remaining) · MandateExpired(validUntil) · MandateNotYetValid(validAfter) · MandateRevoked(hash) · Tripped(hash) · NotAgentKey(caller, agentKey) · InvalidSignature() · InvalidNonce(expected, provided) · SpendExceedsDeclared(actual, declared) · NotAttestor() · InvalidScore(score) · InvalidWindow(start, end) · StaleReport(agentId, windowEnd, last) and the rest.

EIP-712 and hashing helpers

hashMandate(m), mandateDigest(m, chainId, registry), mandateTypedData(m, chainId, registry), revokeTypedData(...), executeTypedData(...), flattenTargets(targets), toSelector("buy(address,uint256)"), MANDATE_TYPEHASH, MANDATE_TYPE_STRING. The SDK's hash equals MandateRegistry.hashMandate byte for byte; the anvil e2e asserts it.

Addresses and ABIs

deployments[chainId], addressesFor(chainId), testnetDemo (demo asset and venue), MONAD_TESTNET_CHAIN_ID, P256_PRECOMPILE, and every ABI: MandateRegistryAbi, MandateExecutorAbi, RiskBreakerAbi, PasskeyAccountAbi, SignerAccountAbi, PrivateSubmitterAbi, ERC8004ReputationAdapterAbi, CREAttestationReceiverAbi, ERC8004IdentityRegistryAbi, ERC8004ReputationRegistryAbi.

@ibxlab/mandate/prf

Call Notes
PRF_NAMESPACE.policy(principal, nonce) / .agentIdentity(agentId) The two namespaces; salt is sha256(namespace)
prfSupported() true / false / null (browser cannot say)
evaluatePrf({ rpId, credentialId?, namespace }) One ceremony; returns { credentialId, output }
encryptPolicy({ rpId, credentialId?, principal, nonce, policy }) { vault, policyHash }; AES-256-GCM under an HKDF key from the namespace's PRF output
decryptPolicy({ rpId, vault }) { policy, policyHash, credentialId }; throws on the wrong passkey or a tampered vault
deriveAgentIdentity({ rpId, credentialId?, agentId }) { address, account (viem LocalAccount), session, end() }
policyHashOf(vault) / parsePolicyVault(json) / policyVaultCanonical(vault) Also exported from the main entry for servers

@ibxlab/mandate/privy

createPrivyIntegration({ appId, appSecret, authorizationKey, keyQuorumId, chainId, addresses }) returns agents.createWallet / account / mirrorMandate / revokeMirror / probe / listWallets / setRevokedPolicy and sessions.embeddedWallet / createScopePolicy / signTypedData / principal. Pure builders buildMandatePolicy, revokedPolicyRules and buildSessionSignerPolicy produce the policy JSON without network calls and are unit-tested.