Wallet adapters

Adapters execute already-authorised intents. They contain no policy logic; authority lives entirely in the gateway, and every adapter refuses to execute an intent whose receipt does not show an APPROVED decision.

Interface

interface WalletAdapter {
  providerName: string;
  validateConnection(config: unknown): Promise<ValidationResult>;
  simulate(intent: AuthorisedIntent): Promise<WalletSimulationResult>;
  execute(intent: AuthorisedIntent, authorisationReceipt: EvidenceReceiptRef): Promise<WalletExecutionResult>;
  getStatus(externalReference: string): Promise<WalletExecutionStatus>;
}

execute returns PENDING | CONFIRMED | FAILED with a provider transaction reference. Callers poll getStatus for pending transactions, then report the outcome so the budget reservation is resolved — adapter errors therefore never corrupt budget state.

Reporting the outcome

There is exactly one flow, and which call ends it depends on what the provider did — not on how long ago you claimed the artifact:

Outcome Call Effect on the reservation
The money moved POST /v1/decision-intents/{id}/confirm reserved → consumed
The provider rejected or the transaction failed POST /v1/authorisation-artifacts/{id}/fail released, including a hold that has already gone UNRECONCILED
You are abandoning the action entirely, before any artifact was claimed POST /v1/decision-intents/{id}/cancel released, and the decision is terminal

/fail is the failure path, not /cancel. Once an executor has claimed an artifact, mnd8t can no longer assume the money stayed put, so /cancel returns 409 for as long as that claim is unaccounted for — cancelling releases budget, and releasing budget for a payment that may have gone out is the one thing the reservation model must not do. /fail is the call that says "the money definitely did not move", and it is the only way an UNRECONCILED hold comes back.

After a /fail you can retry the same decision with a fresh artifact (POST /v1/decision-intents/{id}/reissue-artifact), because failing an artifact leaves an active reservation alone. Call /cancel only when you are giving up on the action; it is terminal, and a later reissue returns 409. See the policy model for the full state table.

Simulated adapter (SIMULATED)

Deterministic and replayable for demos/tests, driven by the destination string: contains fail → fails; contains delay → PENDING for two status polls, then CONFIRMED; otherwise immediate success.

EVM testnet adapter (EVM_TESTNET)

Environment configuration (see .env.example):

Variable Meaning
WALLET_TESTNET_ENABLED feature flag, default false
WALLET_TESTNET_RPC_URL e.g. https://sepolia.base.org
WALLET_TESTNET_CHAIN_ID 84532 (Base Sepolia) or 11155111 (Sepolia)
WALLET_TESTNET_PRIVATE_KEY signer key — testnet funds only
WALLET_TESTNET_TOKEN_ADDRESS optional ERC-20 (testnet USDC); empty = native

Safety rails, enforced in code and covered by tests:

  • disabled unless the flag is on;
  • refuses any chain ID that is not a recognised testnet (mainnet is rejected even if configured);
  • refuses execution without a valid authorisation artifact;
  • the signing key stays inside your executor's environment — never logged, never transmitted, and never visible to mnd8t.

End-to-end example

A reference executor runs the full loop — decision → verification → claim → adapter execution → status polling → confirmation or /fail. Without testnet configuration it uses the simulated adapter, so you can watch the whole flow with no credentials at all.

Download the annotated source — no account contact needed:

Download reference-executor.ts

npm install @mnd8t/sdk
MANDATE_API_KEY=mdt_test_... MANDATE_API_URL=https://api.your-workspace.example \
  npx tsx reference-executor.ts

Standalone, swap its @mandate/wallet-adapters import for your own provider call — that is the one line every real integration replaces anyway. Inside a clone of the repository the adapters are already there: the same file is apps/reference-executor, run with pnpm --filter @mandate/reference-executor start.

AC2 adapter (AC2) — scaffold

AC2 is the Algorand Foundation's open, blockchain-agnostic standard for policy-controlled signing with hardware-bound human approvals and credential isolation. In mnd8t's model it is an enforcement transport: mnd8t decides whether an action is within the delegated authority — against versioned policy and accumulated spend — and AC2 is one way your executor then produces the signature, without the agent (or mnd8t) ever holding a key.

The adapter ships as a scaffold: the safety rails are real and tested (disabled by default, refuses unapproved intents, https-only signer URLs, fails closed), while the wire calls to an AC2 signer live behind the Ac2Transport seam and are unimplemented until a released AC2 specification version is pinned. Every submission carries the mnd8t evidence reference (receiptId, receiptHash) so the signer — or the human approving on their device — can independently verify what was authorised.

Adding a provider

Implement WalletAdapter for your provider, keep its configuration in environment variables, and cover the safety cases (unapproved artifact, disabled flag) with tests. Nothing on the mnd8t side changes — that provider-neutrality is the point, and it is why one mandate can govern several accounts at once.

7 min read · Need something that isn't here? Browse all documentation