The Mandate SDK throws 5 typed error classes. Every error extends MandateError, which extends the native Error. Use instanceof to handle each scenario precisely.
Copy
Ask AI
MandateError (base: any API error)├── PolicyBlockedError (422: policy rule violated)├── CircuitBreakerError (403: agent emergency-stopped)├── ApprovalRequiredError (202: human approval needed)└── RiskBlockedError (422: risk scanner flagged)
All classes are exported from @mandate.md/sdk. Import them in a single statement:
Copy
Ask AI
import { MandateError, PolicyBlockedError, CircuitBreakerError, ApprovalRequiredError, RiskBlockedError,} from '@mandate.md/sdk';
Order matters. Check specific subclasses before the base MandateError. Here is the complete pattern:
Copy
Ask AI
import { MandateClient, PolicyBlockedError, CircuitBreakerError, ApprovalRequiredError, RiskBlockedError, MandateError,} from '@mandate.md/sdk';const client = new MandateClient({ runtimeKey: process.env.MANDATE_RUNTIME_KEY! });try { await client.validate({ action: 'transfer', amount: '500', to: '0xRecipientAddress', token: 'USDC', reason: 'Payment for API access', });} catch (err) { if (err instanceof CircuitBreakerError) { // Emergency stop. All transactions blocked until owner resets. console.error('EMERGENCY: Circuit breaker active. Halting agent.'); // Notify ops team. Stop the agent loop. Do NOT retry. process.exit(1); } else if (err instanceof RiskBlockedError) { // Destination address flagged by Aegis risk scanner. console.error(`Risky address: ${err.blockReason}`); // Do not retry with the same address. May need manual review. } else if (err instanceof ApprovalRequiredError) { // Transaction needs human sign-off. Poll until decided. console.log(`Approval needed: ${err.approvalReason}`); const status = await client.waitForApproval(err.intentId, { timeoutMs: 3600_000, onPoll: (s) => console.log(`Status: ${s.status}`), }); if (status.status === 'approved') { console.log('Approved. Proceeding.'); } } else if (err instanceof PolicyBlockedError) { // Policy rule violated. Show the decline message. console.log(`Blocked: ${err.blockReason}`); if (err.declineMessage) console.log(`Message: ${err.declineMessage}`); if (err.detail) console.log(`Detail: ${err.detail}`); // Do NOT retry with the same parameters. } else if (err instanceof MandateError) { // Generic API error. Check statusCode. console.error(`API error ${err.statusCode}: ${err.message}`); // 5xx: safe to retry with backoff. 4xx: fix the request. }}
Non-negotiable fail-safe rules. Every Mandate integration must follow these:
Always validate before signing. Never sign or broadcast a transaction without calling validate() first.
Block if API is unreachable. If the Mandate API returns a network error or timeout, do NOT execute the transaction. Block and retry.
Never ignore errors. If validate() throws, the transaction must not proceed. No fallback to unvalidated execution.
Display the block reason. When a transaction is blocked, show the human-readable declineMessage or blockReason to the user or log.
Post events after broadcast. For raw validation flows, always call postEvent() with the txHash after broadcasting. This enables envelope verification.
If the Mandate API is unreachable, your agent must block the transaction. Never fall back to unvalidated execution. A network failure is not permission to skip validation. Treat an unreachable API the same as a rejection.
This is the most critical rule in any Mandate integration. If you cannot reach the API, do not execute the transaction. Block, log the error, and retry later.