01
Get the package
It lives in packages/client of the source repository and is not published to a registry yet. Copy the folder into your project, or point a path alias at it. It needs viem 2.
02
Keys
import { actionHash, actionKey, namespaceId, selectorOf } from "@mutiq/client";
const namespace = namespaceId(creatorAddress, "my-app");
const hash = actionHash({ signer, target, data, value: 0n, scope: "profile-42-rev-7" });
const key = actionKey(namespace, hash);Same inputs give the same key. The scope is how you say that a second send is a second action: use a form revision or an order id, never a timestamp, or every click looks new.
03
Queue and guard
import { createSingleFlight, enqueue, move } from "@mutiq/client";
const flight = createSingleFlight();
function onClick() {
const out = enqueue(queue, { key, actionHash: hash, label: "update", signer, target,
data, value: "0", scope, conflictLabel: "profile:42", deadline: 0 }, Date.now());
queue = out.items;
if (!out.isNew) return; // held and counted
const { promise } = flight.run(key, () => wallet.request({
method: "eth_sendTransaction", params: [{ from: signer, to: target, data }],
}));
promise.then((txHash) => {
queue = move(queue, key, "submitted", Date.now(), { txHash });
});
}Run this code synchronously in the click handler. move throws BadTransition for anything outside the table, which matches the contract.
04
Policy
import { ANY_SELECTOR, evaluate, spend } from "@mutiq/client";
const policy = { strict: true, exclusive: false, paused: false, rules: [
{ target, selector: "0x82ab890a", kind: "allow", window: 3600, maxPerWindow: 5 },
{ target, selector: ANY_SELECTOR, kind: "deny", window: 0, maxPerWindow: 0 },
]};
const decision = evaluate(policy, target, selectorOf(data), nowSeconds, used);
if (decision.code !== "allowed") return decision.reason;
used = spend(used, decision, nowSeconds);Codes: allowed, paused, not-listed, denied, window-full. Local rules protect your own code only. The contract enforces its copy when an action is registered.
05
Preflight
import { runPreflight } from "@mutiq/client";
const report = await runPreflight({ signer, target, data, chainId: 4663 }, rpc);
report.ok; // no check failed
report.checks; // [{ id, label, status: "pass" | "warn" | "fail" | "skip", detail }]
report.gas; // bigint or null
report.feeWei; // bigint or null
report.revertReason; // decoded words, or nullrpc is any (method, params) => Promise. A preflight reads and never sends.
06
Nonces and conflicts
import { findConflicts, nonceReport } from "@mutiq/client";
const report = nonceReport(latest, pending, sentByThisApp, Date.now());
report.next; // next free nonce
report.gaps; // numbers missing below what you sent
report.hints; // [{ level, code, text }]
const groups = findConflicts(queue); // conflict-key, same-nonce, same-function07
State
import { readTxStatus } from "@mutiq/client";
const r = await readTxStatus(rpc, txHash, signer, nonce);
r?.status; // "submitted" | "pending" | "confirmed" | "reverted" | "replaced"A failed read returns null, so keep the last known state.
08
Ledger calls
createNamespace(name, flags) flags: 1 strict, 2 exclusive, 4 paused setRoles(id, account, roles) roles: 1 admin, 2 policy, 4 operator renounceRoles(id) setFlags(id, flags) setRule(id, target, selector, kind, window, maxPerWindow) kind: 0 unset, 1 allow, 2 deny checkPolicy(id, target, selector) -> code register(id, actionHash, conflictKey, target, selector, deadline) -> (key, isNew) submit(id, key, txHash) replace(id, key, newTxHash) confirm(id, key) markReverted(id, key) cancel(id, key) expire(id, key) anyone, after the deadline namespaceOf, rolesOf, ruleOf, windowUsed, actionOf, openOf, keyOfTx, actionCount, keysOf, namespaceCountOf, namespacesOf, computeKey, namespaceIdOf
Failures are custom errors: MissingRole, PolicyRefused, ConflictOpen, BadTransition, TxHashUsed, LastAdmin and others. The ABI and address are at /contracts.json.
Add a gas margin to your estimate. An estimate made in the same second as an action's last change comes out 2,800 gas low, and without a margin the transaction can run out of gas. This app adds 25%.
09
Read endpoints
/contracts.json and /llms.txt are open. The relay at /api/rpc serves this site with a budget per network; for your own app, use your own RPC endpoint.
A hosted Developer API is planned, not built. See the roadmap.