Skip to content

Quickstart

Ten minutes, Monad testnet, one funded key. You will create a passkey principal, grant a scoped mandate to an agent, watch the agent execute within bounds, see an out-of-bounds call rejected before it is sent, revoke, and read the agent's reputation.

0. Prerequisites

  • Node 20 or later and pnpm
  • A Monad testnet key with a little MON from faucet.monad.xyz. The same key pays gas and plays the agent.

1. Run it

git clone https://github.com/aliveevie/mandate && cd mandate/sdk
pnpm install
export PRIVATE_KEY=0x...      # your funded testnet key
pnpm quickstart

You will see an explorer link for every step:

principal account 0xaaa2…d7ba
passkey-signed approve https://testnet.monadexplorer.com/tx/0x0167…
mandate granted 0xb5e1… https://testnet.monadexplorer.com/tx/0xf7cb…
agent executed https://testnet.monadexplorer.com/tx/0xb97c…
spent 100 / cap 500, breaker Armed
typed revert before sending: TargetNotAllowed(0x3eC5…, 0x8679b0c0)
revoked true https://testnet.monadexplorer.com/tx/0xc312…
reputation { score: 100, trips: 0, executed: 0, attestations: 1, erc8004: { count: 1n, value: 100n } }

2. What the script does, in your own code

pnpm add @ibxlab/mandate viem
import { createMandateClient, MandateError, testnetDemo } from "@ibxlab/mandate";
import { privateKeyToAccount } from "viem/accounts";
import { monadTestnet } from "viem/chains";

const client = createMandateClient({ chain: monadTestnet, signer: privateKeyToAccount(process.env.PRIVATE_KEY) });
// Browser: Face ID / Touch ID. Servers and CI: add `software: true`.
const principal = await client.passkey.create({ rpId: "yourapp.xyz" });

const draft = client.mandate.build({
  agentId: 1831n,                                   // ERC-8004 id
  agentKey: "0xAgentExecutingKey",
  targets: [{ address: testnetDemo.venue, selectors: ["buy(address,uint256)"] }],
  asset: testnetDemo.asset,
  spendCap: 500n * 10n ** 18n,
  perBlockCap: 300n * 10n ** 18n,
  maxDrawdownBps: 2000,                             // breaker trips at 20% drawdown
  validUntil: new Date(Date.now() + 86_400_000),
});
const signed = await client.mandate.sign(draft, principal);   // passkey prompt
await client.mandate.grant(signed);                           // one transaction
// later
await client.mandate.revoke(signed.hash, principal);          // immediate
const agent = client.agent.load({ mandateHash, executor: agentSigner });
try {
  await agent.execute({ target, data, amount });   // validate -> simulate -> send
} catch (e) {
  if (MandateError.is(e, "SpendCapExceeded")) { /* args: [requested, remaining] */ }
  if (MandateError.is(e, "Tripped"))          { /* breaker is engaged */ }
}
const state = await agent.state();               // spent, remaining, remainingThisBlock, breaker, active
const rep = await client.reputation.get(1831n);
// { score, trips, executed, pnlBps, window, evidenceHash, attestations, erc8004 }

3. Next

  • The full API table and passkey modes are in the SDK README.
  • How the pieces enforce what they enforce: Concepts.
  • Addresses for your own deployment go in createMandateClient({ addresses }). Testnet defaults are built in.