Whitepaper v1 is out: what coordination holds, and what it cannot

Docs

Wrap the send you already have

The client package is plain TypeScript with viem as its only peer. Every example here uses the same functions the app runs.

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 null

rpc 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-function

07

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.

Ready to coordinate?

Click one button twice and watch one action land in the queue. Then read exactly what it holds and what it cannot.

Not a trading bot · Not a wallet · Not a chain · Your wallet signs every transaction