The SDK, for an afternoon rather than a week

amw-finance-sdk is the same four acts the previous page spells out against the ABI, with the EIP-712 types and the flag decoding already written. Nothing here is new behaviour. Every call below is one the contracts already answer.

It depends on viem and @amw/fixtures, which is where every figure in this project is stated once. No database, no indexer, no Hedera SDK, no server. Read the raw path first if you want to know what it is doing; use this if you would rather not copy typed-data definitions out of a test helper.

Installing it

npm install amw-finance-sdk viem

One bundled ESM file with the figures inlined, its declarations beside it, and viem left external, because an integrator almost certainly has one already and two copies of it in a process is two opinions about what an address is. Nothing else is named: no dependency in the manifest resolves only inside this workspace.

The workspace is still @amw/sdk and the package on the registry is amw-finance-sdk, which is the deployment's own domain. Two refusals shaped that: @amw is somebody else's scope, and npm rejects a bare amw-sdk as too close to aws-sdk. npm run pack:sdk stages the tarball and six tests read that staged tree rather than the script's report of it, because a package that installs and fails on the first import is a defect discovered by whoever trusted this page.

Everything this page says about behaviour is exercised by packages/sdk/src/e2e.test.ts, which drafts a mandate, signs it, registers it, fires actions under it and reads the verdict through this package and nothing else.

The whole of it

import { draft, sign, register, request, deploymentFor, usdc } from "amw-finance-sdk";

    const { MandateRegistry, MandateGuard } = deploymentFor(296);

    // 1, the instrument, in the units a person writes.
    const mandate = draft({
      owner: owner.address, agent: agent.address,
      tokenIn: USDC, tokenOut: EURC,
      perAction: 250_000,      // clause 2.1
      perWindow: 2_000_000,    // clause 3.4
      actions: 12,             // clause 3.5, at most 16
      offPrimaryBps: 2500,     // clause 3.7
      minReturnBps: 9700,      // clause 2.5, measured on what came back
      windowMinutes: 60,
      expiresInHours: 24,      // clause 2.4
    });

    // 2, the owner signs. This is the whole of the authority.
    const signature = await sign(wallet, owner, 296, MandateRegistry, mandate);

    // 3: anyone may submit it. A relayer paying gas for an owner who holds
    //     none is still submitting that owner's mandate.
    await register(wallet, submitter, MandateRegistry, mandate, signature);

    // 4: the agent asks. It never executes: the guard decides, and the venue
    //     class and notional are derived inside it.
    const verdict = await request({ chain, wallet }, MandateGuard, agent, {
      mandateHash, target: venue, amountIn: usdc(240_000),
      minReturnBps: 9700, data: swapCalldata,
    });

    if (verdict.held) console.log(verdict.tier, verdict.clauses); // "tier 2" ["3.4","3.7"]

A breach returns; it does not revert

This is the one thing to get right, and it is why request returns a verdict rather than a receipt. A held action has an action id and an IntentRecorded entry; treating it as a failure discards the record the product exists to produce.

verdict.tierWhat happened
cleanEvery clause satisfied. The action executed
tier 1This action was not permitted: its cap, its pair, its venue, its expiry, its floor
tier 2This action was fine and the sequence was not. The composition case
both tiersBoth, which the guard reports together rather than stopping at the first

verdict.clauses gives what a supervisor cites, ["3.4", "3.7"], and verdict.names the bits the contract raised.

Never treat a hold as a transport failure. Retrying it is how an agent turns one refusal into a queue of refusals. The run ends where a supervisor’s work begins.

draft refuses a mandate the registry would

It throws, naming the clause, rather than handing back a struct that fails three steps later as a revert against a signature somebody has already given:

draft({ ...terms, actions: 20 });
    // Error: this mandate cannot be registered: clause 3.5 admits at most 16
    //        actions: the window is summed exactly, and the ledger it is summed
    //        from is what bounds it

Four bounds are checked, and they are the registry’s own rather than a copy: at most sixteen actions to a window, a clause 2.5 floor no higher than the whole, a window of more than zero seconds, and two legs that are not the same token. The console’s mandate form calls the same function before it asks for a signature, so a person and an integrator meet the same refusal in the same words.

What it does not do

Addresses, committed rather than fetched

An integrator reading an address out of a running service is trusting that service to tell them which contracts they are bound by, and that is the one thing they should not have to trust. So deploymentFor(296) returns addresses compiled into the package, and refuses an unknown chain by name rather than returning undefined into a call that fails later.

The cost of committing them is that a redeployment can leave them behind, and once did: the package named the previous guard for a day after the chain had moved, which is an integrator calling a contract nobody enforces against any more. A test now pins them to the file the deploy script writes.

Proving it

Unit tests say the ABIs and the flag decoding are right; they cannot say the package works. e2e.test.ts does the whole of an integrator’s job through this package and nothing else, draft, sign, register, fire, read the verdict, against a simulated chain, and asserts the product’s own claim through it: two actions execute, the third is held on clause 3.4 with every per-action bound satisfied, and the window afterwards shows the hold debited nothing.

npx hardhat node          # in packages/contracts
    npm run -w @amw/sdk test

Without a node it skips and says so: read the skip count, not only the pass count. It never writes to the deployed guard: that contract has no upgrade path and its recorded incident is what a reader is shown, so a test that wrote to it would be spending the demonstration to prove a helper.

The addresses in deploymentFor are the ones this repository deployed and verified, so the version an integrator installs and the contracts this documentation describes are the same pair.