npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@moesi/settle-zerodev

v0.12.0

Published

ZeroDev session-key settlement adapter for Moesi — implements the abstract Settlement interface over @zerodev/sdk + @zerodev/permissions.

Downloads

872

Readme

@moesi/settle-zerodev

The first-party ZeroDev preset for Moesi. It runs Kernel session-key authorization and execution behind one reusable client.

One owner approval per action-bearing chain can enable a bounded ECDSA or WebAuthn modular operator. The adapter packs ordered deploy and post-deploy actions into bounded ERC-4337 UserOperations as chain state and execution gas limits allow.

Operating-model boundary

The client-level session is a container for chain-local authority. Each chain has its own Kernel derivation/version, permission ID, ENABLE and DEFAULT nonce state, sponsorship/funding route, receipt evidence, revocation transaction, and finality. A repeated address or one detached approval envelope does not make those lifecycles atomic or shared.

The Kernel root owner/admin remains supreme: it approves, administers, and revokes permissions and is not constrained by an operator grant. Ordinary authorize() creates an in-memory ECDSA operator; linkless flows can restore a matching ECDSA or WebAuthn/passkey modular signer. Direct EOA settlement remains outside Kernel policy enforcement.

Returned action receipts are claims for independent verification, not durable completion by themselves. Bind them to the moesi.action-id/v3 hash of the domain, manifest hash, chain key, exact action step, target, calldata, native value, and stable occurrence; check contractName, selector, and predictedAddress separately. Then verify externally reviewed action and execution context, canonical settlement and finality, account/permission attribution, runtime, and postconditions before re-observing the chain. Preserve confirmed prefixes and unresolved hashes rather than resubmitting them on another route.

Sponsored, organization-funded Kernel, owner-submitted EntryPoint, and native/direct EOA routes have different gas payers. Sponsorship exists only when the selected provider and policy accept the operation. Ordinary Kernel grants do not enforce a gas-price ceiling, total gas budget, or mandatory paymaster. Several active grants expose the union of their independently allowed calls and remaining counters.

exact-action supplies reviewed identity. Target, selector, supported arguments, value, recipient, time, operation-count/rate, and configured aggregate constraints are on-chain only when the compiler reports them that way; full-calldata identity and purpose/project/memo remain advisory or off-chain. ERC-7579 core module types are 1–4. Policy type 5 is the draft ERC-7780 extension, not a core portability claim.

Install

npm install moesi @moesi/settle-zerodev viem

Local fork harness

The supported programmatic entry for Moesi's loopback-only development bundler is:

import { startForkBundler } from "@moesi/settle-zerodev/fork-bundler";

const bundler = await startForkBundler({
  rpcUrl: "http://127.0.0.1:8545",
  chainId: 31337,
  executorPrivateKey: process.env.ANVIL_PRIVATE_KEY as `0x${string}`,
});

try {
  console.log(bundler.url);
} finally {
  await bundler.close();
}

It is a deterministic local test harness, not a production ERC-4337 bundler. The separately installed moesi-fork-bundler executable remains part of @moesi/cli, which has no supported JavaScript import surface.

Quick start

import { createMoesi } from "moesi";
import {
  defineZeroDevAuthorizationPolicies,
  zerodevPreset,
} from "@moesi/settle-zerodev";

const moesi = createMoesi(zerodevPreset({ zerodevProjectId }));
const policies = defineZeroDevAuthorizationPolicies({
  policiesByChain,
  resolveActions,
});
const session = await moesi.authorize({
  owner,
  chains,
  policies,
});

const review = await moesi.reviewActions({ chain, session });
if (review.coverage.recommendation === "reauthorize") {
  showAuthorizationReview(review);
}

const result = await moesi.executeActions({
  chain,
  session,
  options: {
    onWarning: warning => showWarning(warning.userMessage),
    onEvent: event => auditTimeline.add(event),
  },
});

if (!result.ok) showDiagnostic(result.diagnostic);

That is the application transaction API on default, Tempo, HyperEVM, and MegaETH chains. The client derives the reviewed owner, Kernel, policy, route, lane, bootstrap, journal, and canonical resolver from composition and the session. It checks adapter receipt mapping and re-resolves after confirmed alternate prefixes internally; callers still independently verify returned receipt claims before counting durable coverage. The caller does not repeat those composition inputs or branch on chain names.

moesi.dryRun(...) uses the same chain-policy estimate loop as execution. Missing direct-owner and Kernel-sender estimates are read from chain.rpcUrl automatically, and RPC-backed policy preflights use that same public reader. Applications may still pass previously reviewed estimates, but do not need to construct an RPC adapter or a temporary signer. Required sponsorship without a usable bundler/paymaster pair returns the same provider-neutral failure before dry-run or authorization; optional and self-funded entries may review an owner route.

reviewActions({ chain, session }) is the reusable no-sign boundary for independently resolved phases. Omit actions to invoke the resolver retained by the session, or pass the newly resolved canonical subset explicitly. The structured coverage reason distinguishes scope-mismatch, expired, revoked, owner-changed, account-changed, chain-changed, and disposed; its recommendation distinguishes reauthorization from restoring wallet context or resolving another blocker.

Before a session exists, review the owner/session decision with the same API:

const review = await moesi.reviewActions({
  chain,
  ownerAddress,
  kernelAddress,
  actions,
  options: { requested: "auto", ownerAvailable: true },
});

The result contains the exact ordered pack boundaries and JSON-safe decimal execution, pre-verification, and total gas evidence used by the settlement packer. Auto mode selects owner only when the complete plan is proven to fit one UserOperation; multiple packs select session, and an already covered session wins without another enable signature. Missing simulation or required sponsorship-stub evidence is explicitly inconclusive. Review never signs, allocates a nonce, touches the alternate-payload journal, requests sponsorship, or submits; executeActions() rechecks all invariants before side effects.

policies is the reviewed account-authority bundle. Resolver-backed applications create it once with defineZeroDevAuthorizationPolicies({ policiesByChain, resolveActions }); the returned session retains that canonical resolver for later re-resolution. See the moesi composition-root guide for the expanded setup.

Linkless operator sessions

The composed client also supports an owner and operator that never share a session secret. The owner derives the exact permission identity from the operator's public material and signs a detached enable approval without submitting a UserOperation:

import {
  createWebAuthnSessionSigner,
  WebAuthnSignerVersion,
} from "@moesi/settle-zerodev";

const publicSigner = {
  kind: "webauthn",
  pubX: webAuthnKey.pubX,
  pubY: webAuthnKey.pubY,
  // This credential-id hash is public and is part of ZeroDev's signer data.
  authenticatorIdHash: webAuthnKey.authenticatorIdHash,
  webAuthnSignerVersion: WebAuthnSignerVersion.V0_0_4_PATCHED,
} as const;

const permissionId = await moesi.recomputePermissionId({
  chainKey: chain.key,
  kernelVersion: "0.3.1",
  policies: policies.policiesByChain.get(chain.key)!,
  publicSigner,
});

const approval = await moesi.buildDetachedEnableApproval({
  owner,
  kernelAddress,
  accountIndex, // exact CREATE2 salt used to create this Kernel
  chains: [chain],
  policies,
  publicSigner,
  kernelVersion: "0.3.1",
});

await approvalStore.save({ permissionId, approval });

The serialized approval contains the owner-signed enable data, public signer identity, Kernel binding, and policy-derived permission ID. It does not contain an operator private key and does not spend gas.

Every detached approval uses the single moesi.zerodev-detached-approval/v1 schema. The required kind field is the model discriminator:

  • kind: "per-chain" contains chains and no grant.
  • kind: "replayable" contains one compact grant and no chains.

The v1 codec is exact: both models at once, neither model, an absent/unknown kind, or unknown envelope/record fields are rejected before any chain read or authority restore. There is no v2 approval and no compatibility reader for undiscriminated or extended payloads; create a new approval instead.

Inspect and diagnose stored approvals

Treat a detached approval as opaque application data. Use the root-package inspector when a database row must be classified or displayed without exposing the owner signature or serialized account:

import { inspectDetachedApproval } from "@moesi/settle-zerodev";

const inspection = inspectDetachedApproval(storedApproval);
if (inspection.status === "reapproval-required") {
  showOwnerReapprovalFlow();
} else if (inspection.status === "invalid") {
  quarantineStoredValue();
} else {
  showApprovalBinding(inspection.binding);
}

inspectDetachedApproval() always returns the immutable moesi.zerodev-detached-approval-inspection/v1 result. Branch on status, kind, and replayableEnable; reason is diagnostic prose. A supported per-chain approval has one binding only when every chain record agrees on the owner, Kernel, version, index, permission, and signer. useMetaFactory is null for per-chain records because that serialized-account model does not encode the factory choice.

The tooling subpath can additionally reproduce a current compact replayable WebAuthn ENABLE and compare it with one selected chain. The package owns signer, policy, permission, typed-data, signature, Kernel factory, nonce, and root-owner reconstruction. The caller supplies the public expected binding and a chain-bound viem client:

import {
  createDetachedApprovalReadinessReader,
  diagnoseDetachedApprovalReadiness,
} from "@moesi/settle-zerodev/tooling";

const reader = createDetachedApprovalReadinessReader({ client: publicClient });

const readiness = await diagnoseDetachedApprovalReadiness({
  approval: storedApproval,
  chainId: chain.chain.id,
  expectedOwnerAddress,
  expectedKernelAddress,
  expectedAccountIndex: accountIndex.toString(),
  expectedPermissionId: permissionId,
  passkey: {
    pubX: webAuthnKey.pubX.toString(),
    pubY: webAuthnKey.pubY.toString(),
    authenticatorIdHash: webAuthnKey.authenticatorIdHash,
  },
  scope: {
    targets: [{ target: factory, selector: deploySelector }],
    expiresAt,
    dailyDeployLimit,
  },
  reader,
});

if (readiness.issue) {
  routeReadinessAction(readiness.issue.code, readiness.issue.action);
}

Readiness results use moesi.zerodev-detached-approval-readiness/v1. Machine decisions consume the closed issue.code, issue.action, and issue.retryable fields, never issue.message or provider error text. An absent TimestampPolicy alone is the retryable SESSION_KEY_TIMESTAMP_POLICY_INSTALL_REQUIRED; unreadable RPC state, other missing modules, unavailable factory routes, an expired scope, a changed proxy implementation, an exact consumed permission ENABLE nonce, changed validator nonce, and changed root authority remain distinct categories. The reader first rejects a mismatched client chain, then opens and closes one exact canonical block hash/number while binding every deployment, outer EntryPoint factory, underlying KernelFactory approval, sender, implementation, exact permission nonce, and authority read to that hash with EIP-1898 requireCanonical. This API diagnoses only: it does not submit, install, restore, revoke, or retry an approval.

| Stored detached generation | Inspection | Readiness diagnosis | | --- | --- | --- | | Current exact kind: "replayable" compact grant | supported | Supported for the patched WebAuthn v0.0.4 signer and the documented target/selector, optional expiry, and optional with-reset daily-rate scope | | Current exact kind: "per-chain" records | supported | Not supported; continue to the ordinary per-chain restore path | | Pre-discriminator v1 (replayable or per-chain) | reapproval-required | Reapprove with the current builder | | Per-chain record carrying the retired replayable marker | reapproval-required | Reapprove with the current builder | | Replayable record carrying a retired serialized account | reapproval-required | Reapprove with the current builder | | Malformed, ambiguous, unknown-version, or extended input | invalid | Reapprove or quarantine; no chain read is attempted |

Later, the operator restores that approval with its live signer. The restored authority uses the same reviewed resolver and executeActions() path as an ordinary session. Its route is fixed to the captured bundler transport, so it cannot fall back to an unavailable owner signer:

const signer = await createWebAuthnSessionSigner({
  chain,
  webAuthnKey,
  webAuthnSignerVersion: WebAuthnSignerVersion.V0_0_4_PATCHED,
});

const session = await moesi.restoreSessionFromApproval({
  approval,
  signer,
  accountIndex,
  chains: [chain],
  policies,
});

const result = await moesi.executeActions({ chain, session });

createWebAuthnSessionSigner() performs no chain read and prompts the passkey only when execution signs a UserOperation. The first operation uses ENABLE mode with the stored owner approval; later operations rebuild the same permission in regular mode. For a replayable grant, the adapter presents the replayable signature marker on both the ENABLE gas-estimation stub and the completed submitted signature. It derives that mode from the validated grant; application transports do not inspect the nonce or rewrite JSON-RPC requests. DEFAULT-mode estimation and submission remain chain-bound and unmodified. recomputePermissionId() is pure and Kernel-version independent, while still accepting kernelVersion to make the account configuration explicit. accountIndex must be the same non-negative bigint used when the Kernel was first derived; omitting it preserves the 0n default.

When the one-time detached approval has already been retired, restore only from canonical installed state. This path recomputes the permission ID from the exact policies and live signer, reads the exact signer material from its on-chain module, and reconstructs a steady-state Kernel plugin without an enable signature:

const session = await moesi.restoreInstalledSession({
  signer,
  owner, // independently derives the Kernel and supports a later session.revoke(...)
  kernelAddress,
  permissionId,
  accountIndex,
  chains: [chain],
  policies,
});

Before reading signer storage, restoration independently derives the Kernel from the owner, Kernel version, and account index through the canonical factory route; the caller-supplied address cannot short-circuit that proof. Missing signer storage, a permission or account binding mismatch, reconstruction failure, a non-default steady-state nonce, and validation or observation failure all throw InstalledPermissionRestoreError. Branch on its closed reason category rather than diagnostic prose. This API does not infer that a pending installation succeeded and does not replay a retired approval.

Experimental raw P-256 signer

P256PermissionSigner and createRawP256SessionSigner() are explicitly experimental. They have not received an independent contract/security review, do not have a canonical deployment registry, and are not a complete OAuth or persistent-device-key integration. Keep production use disabled until those separate reviews and integration proofs exist.

The application deploying the module owns its deployment on every chain. Until the upstream deployment catalog work tracked by #228 assigns a durable owner, record the chain ID, immutable source revision, compiler settings, runtime bytecode hash, deployment transaction, deployer/operations owner, and independent review beside the explicit signerContractAddress. The library deliberately has no ambient default address.

Before retaining a live signer, createRawP256SessionSigner() performs three read-only checks against the supplied chain: exact RPC chain ID, non-empty code at the explicit signer address, and a known-valid RIP-7212/EIP-7951 vector at precompile address 0x100. It then asks signPayload to sign a fresh random challenge under the moesi.raw-p256-key-consistency/v1 local domain and verifies that proof against the exact pubX/pubY. A wrong HSM/browser key or a replayed fixed proof therefore fails before an approval is restored or a UserOperation is assembled. Use probeRawP256Capability() directly when presenting readiness diagnostics. Signer construction performs one extra signPayload call for this local proof and discards its signature; it does not install or expand authority.

const signer = await createRawP256SessionSigner({
  chain,
  pubX,
  pubY,
  signerContractAddress, // explicit reviewed deployment; no default
  signPayload: payload =>
    crypto.subtle
      .sign({ name: "ECDSA", hash: "SHA-256" }, privateKey, payload)
      .then(bytes => new Uint8Array(bytes)),
});

The experimental wire contract is moesi.raw-p256-kernel/v1:

| Field | Normative value | | --- | --- | | Curve | NIST P-256 / secp256r1 | | Input H | Kernel's exact 32-byte validation hash | | Domain | The upstream Kernel/EntryPoint UserOperation hash or ERC-1271 caller hash; Moesi adds no EIP-191, WebAuthn, or custom prefix | | Verification digest | SHA-256(H) because WebCrypto ECDSA hashes the supplied H once | | Signature | Exactly 64 bytes: unsigned big-endian IEEE-P1363 r[32] || s[32] | | Canonicality | 0 < r < n and 0 < s <= n/2; high-s is normalized client-side and rejected on-chain | | Rejected encodings | ASN.1 DER, variable-width integers, recovery bytes, WebAuthn assertion envelopes |

The public cross-runtime vector in test-vectors/raw-p256-kernel-v1.json pins H, SHA-256(H), affine public coordinates, and the accepted low-s r || s bytes. TypeScript/WebCrypto and Foundry tests both consume or mirror that vector. Changing the hash layering, domain, coordinate encoding, or signature encoding requires a new scheme/vector version and a separately reviewed module deployment.

Registry grant records currently retain only a permission ID, so the default Kernel observer conservatively checks the ordinary ECDSA signer-storage path. Consumers that retain the signer kind can use readSignerMaterial() or isSignerInstalled() from @moesi/settle-zerodev/tooling for an explicit ECDSA, WebAuthn, or raw-P256 storage check. WebAuthn/raw-P256 registry reconciliation remains inconclusive when that signer identity is unavailable.

For owner-side revocation, create a public-only operator identity and supply the live owner while restoring. The resulting session retains owner authority only for its existing session.revoke(...) boundary:

import { createPublicSessionSigner } from "@moesi/settle-zerodev";

const revocable = await moesi.restoreSessionFromApproval({
  approval,
  signer: createPublicSessionSigner(publicSigner),
  owner,
  accountIndex,
  chains: [chain],
  policies,
});

await revocable.revoke({ chainKey: chain.key, action, actionIndex });

Conclusive detached revocation evidence

Submission and inclusion are not proof that the intended permission is gone. After the owner-authorized revocation yields a transaction hash, verify the opaque approval against the authenticated chain reader before persisting a revoked state:

import {
  verifyDetachedApprovalRevocation,
  type DetachedGrantRevocationEvidence,
} from "@moesi/settle-zerodev";

const evidence: DetachedGrantRevocationEvidence =
  await verifyDetachedApprovalRevocation({
    client: publicClient,
    approval,
    chainId: chain.chain.id,
    transactionHash,
  });

await auditStore.put(JSON.stringify(evidence));

The verifier consumes the package-owned approval inspection/codec, checks the RPC chain and canonical EntryPoint transaction, and recomputes the hash-covered UserOperation. It requires exactly one successful operation on the supported version-specific DEFAULT/SUDO ECDSA root nonce lane, whose uninstallValidation(bytes21,bytes,bytes) input carries the approval's exact bytes4 permission and canonical policy/signer data. It then requires the exact ordered Kernel ModuleUninstallResult(address,bool) and ModuleUninstalled(uint256,address) pairs for every policy and signer module.

Compatibility note: Kernel 0.3.1–0.3.3 ABIs contain PermissionUninstalled(bytes4), but the deployed supported runtimes do not emit it. That ABI-only declaration is not accepted as evidence. The verifier instead binds the permission through canonical calldata and the successful EntryPoint event, then binds the actual modules through runtime-emitted events.

Finally it pins the number and hash of a fresh block at or after inclusion, reads the approval's ECDSA, WebAuthn, or raw-P256 signer store at that explicit block number, and requires the same number/hash to remain canonical after the read. The evidence retains both observation fields. Unreadable storage, any nonzero material (including different or reinstalled material), a reorged receipt or observation block, another validator lane, or missing/mismatched operation or module evidence fails with DetachedApprovalRevocationVerificationError.reason; consumers never branch on diagnostic prose.

Evidence uses decimal strings for block numbers and contains no approval, enable signature, signer identity, or reusable authority. It supports detached approval-v1 and Kernel 0.3.1–0.3.3. authorityScope is derived from the approval: recorded-chains for per-chain approvals and any-chain-where-account-exists for replayable approvals. The returned artifact proves absence on exactly the reported chain and observation block; it does not turn finite checks into global revocation. RPC authentication, confirmation or finality policy, durable retention, fleet reconciliation, and recovery remain caller responsibilities.

Detached approvals currently support the built-in Call, Timestamp, and Rate Limit policy serialization used by linkless grants. Custom aggregate-outflow policies remain on the ordinary in-memory authorization path.

Durable alternate-lane recovery

The preset configures restart-durable recovery automatically for every built-in alternate lane. Ordinary UserOperations do not need an alternate-payload journal. In Node the preset uses FileAlternatePayloadJournal at .moesi/alternate-payloads, resolved from process.cwd(). Node applications using that default should add /.moesi/ to their .gitignore; this repository already does. In browsers the preset uses origin-scoped localStorage guarded by Web Locks. Native bootstrap, native-owner, EntryPoint self-bundle, and UserOperation execution therefore use the same application API without a journal argument.

Pass a journal override only to select a custom storage backend, directory, or retention policy:

import { FileAlternatePayloadJournal } from "moesi/node";

const moesi = createMoesi(zerodevPreset({ zerodevProjectId }), {
  journal: new FileAlternatePayloadJournal({
    directory: "/var/lib/my-app/moesi-alternate-payloads",
  }),
});

The later authorize({ owner, chains, policies, account? }) and executeActions({ chain, session, options }) calls do not change. The automatic browser backend fails closed if localStorage or Web Locks is unavailable; other runtimes must supply a durable journal override. An in-memory map cannot quarantine uncertain submissions across reloads or process restarts.

Low-level adapter APIs

New applications should compose through createMoesi(zerodevPreset(...)). The root package exposes that preset, reviewed policy bundles, and the high-level linkless signer/approval helpers described above. The earlier prepareAccounts() / authorizeSession() application workflow, the catch-all advanced entry, and the positional executeActions() overload are deleted. Transaction call sites use the client object form shown above.

Low-level policy reviewers and adapter authors can explicitly import executeZeroDevWithChainPolicy() and prepareZeroDevNativeBootstrap() from @moesi/settle-zerodev/tooling. Owner, threshold, signer, session-policy, and policy-compilation operations live under @moesi/settle-zerodev/administration. Those primitives intentionally stay out of root-package autocomplete and are not application transaction APIs.

computePermissionId() on the administration subpath derives the canonical ZeroDev permission ID from reviewed policies and public signer material without a chain client or permission plugin. The tooling subpath exposes canonical ECDSA/WebAuthn signer-storage read-back plus bounded, confirmation-aware listWebAuthnOperatorRegistrations() historical log enumeration. Registration history is discovery evidence only; use signer-storage read-back to establish whether a permission is currently installed. Historical enumeration is all-or-nothing: if the head read or any bounded getLogs page/range fails, the whole promise rejects and no partial registration list is returned. Callers may reduce chunk and retry the complete requested range; a progress/partial-result surface is intentionally omitted so incomplete discovery cannot look complete.

import { computePermissionId } from "@moesi/settle-zerodev/administration";
import { readSignerMaterial } from "@moesi/settle-zerodev/tooling";

const permissionId = computePermissionId(policies, publicSigner, { chainKey });
const installedSigner = await readSignerMaterial({
  client: publicClient,
  permissionId,
  kernel,
  kind: "ecdsa",
});

The permission-ID implementation mirrors the exact toPermissionValidator().getIdentifier() algorithm resolved in this lockfile: @zerodev/[email protected] running with @zerodev/[email protected]. It ABI-encodes toPolicyId(policies), PolicyFlags.FOR_ALL_VALIDATION, and toSignerId(signer), hashes the result, and takes the first four bytes. The direct upstream parity test and the fixed packed-consumer result fail when that algorithm changes.

This is temporary compatibility ownership tracked by #228, not a permanent fork of ZeroDev protocol knowledge. Replace these helpers with upstream APIs and remove the local permission-ID/signer-storage ABI mirror once a supported ZeroDev release exposes both (1) chain-client-free permission-ID derivation from the same public inputs and (2) typed ECDSA/WebAuthn installation evidence that preserves unreadable/unsupported state. The replacement must pass the existing fleet fixtures and direct parity tests before callers migrate.

The adoption smoke builds the matching consolidated moesi runtime, packs it and this package, installs both tarballs in a temporary npm consumer with the exact ZeroDev versions above, rejects workspace symlinks/imports, and executes both documented subpaths with a mocked read-only signer contract call:

pnpm --filter @moesi/settle-zerodev build
pnpm --filter @moesi/settle-zerodev smoke:packed-consumer

gasBudget: { domain: "execution", maximum } bounds the internal session-key packing budget while intentionally excluding preVerificationGas. Select domain: "total" when the ceiling includes it. Packing diagnostics remain internal and are not part of the ordinary object-form result.

The result attributes every action to its selected lane and confirmed receipt.

For a fresh permission, initialVerificationGasLimit sets a floor on the initial ENABLE/bootstrap UserOperation before sponsorship or signing. The adapter verifies that middleware did not lower the prepared value and never copies the floor to later DEFAULT-mode operations. On chains where even a large EntryPoint verification allowance cannot cover Kernel code deposit, bootstrapOwnerKernelAccount() derives the same owner-sudo factory call, sends it as a normal owner transaction, waits for its native receipt, and requires fresh RPC reads to observe code at the deterministic Kernel address. prepareOwnerKernelFactoryDeployment() exposes the unsigned factory target and calldata for policy orchestrators that submit native transactions through their own wallet adapter.

Automatic chain-policy execution

executeActions({ chain, session, options }) is the normal transaction API everywhere. The adapter selects the default, Tempo, HyperEVM, or MegaETH policy from the ChainEntry; derives owner, Kernel, version, and account index from the retained session; and prepares any exact MetaFactory bootstrap internally. Applications do not import policies or branch on chain names at the send callsite.

Signer and sender routing remain automatic when omitted. Developers may hard-pin either dimension independently through options.signMethod ("owner" or "session-key") and options.transactionSender ("bundler" or "owner"). They are independent selection axes, but not every pair is supported: signMethod: "session-key" with transactionSender: "owner" fails closed because the owner-submitted EntryPoint path cannot safely submit a session-signed operation. A hard pin is either honored exactly or rejected; the preset never silently changes it.

Application onEvent callbacks receive the versioned moesi.settlement-event/v1 contract. Its variants and fields are additive-only within v1; consumers should branch on schemaVersion before kind and retain the version with persisted audit events. Adapter-facing unversioned inputs are sanitized, frozen, and versioned by createMoesi() before delivery.

The high-level authorize({ owner, chains, policies, account? }) path snapshots the owner, optional account binding, canonical resolver, and bound journal methods before authorization awaits. Initial and post-receipt plans therefore use one resolver and one durable quarantine for the lifetime of the authority. resolveActions belongs inside defineZeroDevAuthorizationPolicies({ policiesByChain, resolveActions }); it is not a top-level authorization field. Every returned action must match the requested chain. An empty initial resolution is an explicit no-op and performs no wallet, bootstrap, or journal operation.

Existing Kernels created with a nonzero salt can bind the original CREATE2 index and recorded address directly on the composition root:

const session = await moesi.authorize({
  owner,
  chains,
  policies,
  account: {
    index: BigInt(kernelSalt),
    expectedAddress: recordedKernelAddress,
  },
});

The preset derives from owner + kernelVersion + index without trusting the address pin. If the result differs from expectedAddress, authorization fails before a session key is generated or the wallet signs. Omitting account preserves the historical index 0n behavior.

session.getReviewedChain(chain.key) returns only frozen, credential-free review evidence: chain key/id/name, Kernel address and account index, permission and nonce evidence, policy constraints, sponsorship posture, and coarse transport configuration. It never returns RPC, bundler, paymaster, or fallback URLs; session private keys; serialized accounts; opaque native policy objects; or a direct transaction method. Exact reviewed routes and executable authority remain enforced inside a non-reflectable package capability and are erased when the session is disposed.

For native bootstrap, the adapter commits the owner, chain, Kernel, factory, exact calldata/value, Kernel version, account index, and MetaFactory route to the canonical settlement identity. It submits those same prepared bytes, waits for an attributed successful receipt, and verifies fresh Kernel code. The deterministic SDK factory recipe remains available after the Kernel is already deployed, so repeated executions reach the existing code check and continue without resubmitting the bootstrap.

For mixed plans, direct owner estimates are produced only for explicitly sender-independent deployments. Sender-bound calls are encoded through the exact owner-sudo Kernel account and estimated with EntryPoint as caller. Native-owner transactions are receipt- and transaction-attributed to the reviewed sender, chain, target, calldata, value, gas, and fee envelope. Guarded Kernel actions are grouped into one unsponsored owner-signed EntryPoint v0.7 UserOperation with explicit gas fields. The adapter reads EntryPoint balanceOf, confirms a depositTo(kernel) shortfall transaction when needed, and signs the exact encoded calls. Automatic submission sends that same owner-signed UserOperation to the bundler first; direct owner-sent handleOps is eligible only after a conclusive pre-acceptance rejection. Success is accepted only after the unique matching successful UserOperationEvent is verified.

Every alternate payload is reserved before submission. Ambiguous native, deposit, or handleOps outcomes remain pending (and are enriched through recordUncertain when the journal supports it), so automatic retry cannot duplicate a transaction. After each confirmed alternate receipt the caller's resolver runs against canonical state, while the same session is retained for the final session continuation. Any failed pack, failed actions inside one pack, explicit unattempted suffix, duplicate mapping, or missing action result rejects. Successful results expose stable action identity, occurrence, alternate payload identity, lane, and canonical receipt mapping.

Low-level policy integrators can use the explicit @moesi/settle-zerodev/tooling subpath. Those orchestration primitives are intentionally absent from the root package autocomplete.

Settlement preflight

preflightZeroDevSettlement() publishes moesi.settlement-preflight/v1 with ready | blocked | inconclusive aggregate and per-check states. Every check records whether it is required, its reason, evidence source, provider, observation/expiry time, and sanitized values. The preflight reuses the same route, sponsorship, UltraRelay, gas-domain, and provider-capacity vocabulary as execution.

Profiles are explicit: read-only (default) issues only JSON-RPC reads, plan-simulation accepts a caller-supplied callback whose result is labeled caller-attested (the package cannot prove what that callback did), and active-canary requires both a literal confirmation and callback. Passive bundler reachability never proves relay inclusion, a paymaster stub never promises sponsorship, and UltraRelay EntryPoint support never proves project entitlement. Unknown capacity or gas domains remain inconclusive. A positive self-funded balance is likewise inconclusive until simulation supplies a full requiredPrefund; known shortfalls block and covered prefund is ready. Preflight accepts the same explicit gasBudget: { domain, maximum } contract as execution. Any blocked required check blocks; otherwise any inconclusive required check keeps the report inconclusive.

Failure diagnostics

Every ZeroDev { ok: false } result carries a structured, frozen diagnostic, and every ZeroDev failed event mirrors it while retaining the existing reason string. ZeroDev diagnostics carry the prepare/submit/receipt stage, safe call shapes, sender deployment evidence, factory/paymaster-data presence, nonce mode, permission identifier, redacted route, sponsorship mode, and a UserOperation hash once submission is known. phase remains unknown unless an EntryPoint response or mined receipt proves validation or execution.

Prepared detached/session UserOperation failures also carry diagnostic.submission. Applications may issue another execution only when that closed disposition positively says retrySafe: true (not-started or signature-requested). A completed signature is already conservative and non-retry-safe and preserves the locally computed operation hash before formatting, cancellation checks, or dispatch. Entering eth_sendUserOperation produces submission-attempted with the deterministic hash even when the provider returns a structured JSON-RPC or HTTP rejection; accepted operations advance to submitted, and a canonical receipt advances to receipt-observed with both hashes. Missing or rejected lifecycle evidence is inconclusive, never permission to retry. Owner-signed alternate self-bundle execution has a separate lifecycle contract and does not gain this field.

After a submitted receipt timeout, observe the exact hash without entering a send path:

const observation = await moesi.observeUserOperation({
  chain, // must retain the exact caller-owned bundlerTransport used to submit
  userOpHash: result.diagnostic.submission.userOpHash,
  accountAddress: expectedKernelAddress,
  nonce: submittedNonce, // optional when the exact submitted nonce was retained
});

The method performs one eth_getUserOperationReceipt request, verifies its transaction receipt and containing canonical block through the chain read route, and returns frozen pending, included, finalized, or unreadable evidence. Included and finalized results distinguish an inner success from failed; the fixed EntryPoint event must bind the requested hash and Kernel, plus the nonce when supplied. Finalization carries the canonical finalized block number and hash. Null receipts stay pending, while malformed evidence and read failures retain the operation hash as unreadable rather than claiming it was dropped. It never submits, waits, mutates the local nonce cursor, or treats observation alone as permission to reuse a quarantined nonce.

Diagnostic output never includes calldata/request bodies, signatures, session or serialized-account material, factory calldata, paymaster blobs, credentials, authorization/cookies, or raw nested SDK errors. Redaction is recursive across nested causes. Empty errors still include operation context and an honest unreadable-error cause. Moesi does not replay individual inner calls: that would not reproduce EntryPoint, Kernel, counterfactual-deployment, or paymaster validation and cannot replace the original provider response.

Sponsorship intent

Every authorization carries explicit required | optional | self-funded sponsorship intent. Required mode fails before UserOperation preparation or submission when the selected configured route lacks a usable paymaster. Self-funded mode disables UltraRelay and never installs paymaster middleware, even if paymaster fields are present. Optional mode prefers the configured sponsor, but may retry preparation without it only before any submission; the adapter then reads the Kernel balance and proves it covers the prepared operation's complete prefund before sending. An insufficient balance blocks, and a failed balance read is inconclusive rather than risking AA21.

Every chain entry must declare sponsorshipMode; URLs never imply sponsorship or paymaster intent. normalizeSponsorshipIntent validates the explicit mode and selected paymaster capability.

UltraRelay and configured fallback

ultraRelay: "auto" is the default. For a ZeroDev RPC, the adapter probes the provider=ULTRA_RELAY route for EntryPoint 0.7 and uses it when available; it otherwise retains the configured bundler and paymaster path. UltraRelay combines those services, so the adapter omits separate paymaster middleware on that route. Because the reachability probe cannot prove project entitlement, "auto" also retries one explicit availability or sponsorship failure through the original configured bundler/paymaster only when it occurs before signing or entering the send request. Every entered send is quarantined and never falls back, including structured provider rejections. Use "never" to pin the configured path or "require" to fail without fallback if UltraRelay is unavailable. Availability probes are coalesced and refreshed; a transient miss falls back safely and is probed again on a later execution. Non-ZeroDev/BYO bundler URLs are never rewritten.

Chain-state read pools

Settlement chain-state reads use the same ChainEntry read-pool contract as the resolver. Optional rpcUrls, rpcStrategy, rpcCooldownMs, and rpcTimeoutMs settings are carried through authorization and used for Kernel preparation, permission/nonces, account reconstruction, revocation state, EOA balances, and receipt reads. Transport failures and HTTP 408/429/5xx responses may fail over; successful JSON-RPC application errors do not. Concrete block parameters are replayed unchanged, and endpoint credentials are redacted from transport diagnostics.

Bundler, paymaster, UltraRelay, wallet transaction, raw transaction, and UserOperation submission routes are not added to this chain-state pool. Their selection and retry boundaries remain independent.

Nonce sequencing and stale account state

Authorization reads the permission nonce once and snapshots the separate Kernel DEFAULT-mode counter before the first send. executeActions then owns that logical sequence locally and serializes concurrent callers that share the same chain, EntryPoint, account, and permission key. EntryPoint consumes one nonce per packed UserOperation, regardless of how many actions are inside it. Different sequence keys can still execute in parallel.

Fresh permissions start with one ENABLE-mode operation and then move to the snapshotted DEFAULT key. Later operations are rebuilt with the permission marked pre-installed and omit factory/authorization data. Account-state reads stay on the public read pool while bundler and paymaster routing remains independent. If stale state produces AA10 or AA25 during the ENABLE-to-deployed transition, the adapter reads the current DEFAULT counter from EntryPoint and rebuilds once without factory data; the exact stale payload is never blindly resubmitted. ENABLE-mode AA23 remains the original terminal permission error and cannot advance the cursor. Steady-state AA23/AA25 propagation failures receive bounded byte-identical retries.

An accepted UserOperation whose receipt remains unresolved—or any send whose transport outcome is ambiguous—makes the local nonce cursor uncertain and blocks automatic reuse. The adapter signs and submits once; it never fallbacks, repacks, or resubmits after a returned hash or ambiguous transport outcome. It computes the canonical hash locally even when the bundler never returns one. Reconcile that hash first, then pass an explicit nonceSeed only when the canonical EntryPoint counter is known. receiptTimeoutMs is one absolute wall-clock deadline (default 120 seconds), not a per-retry allowance; timeouts include the hash, redacted route, nonce key, submission time, and deadline. Public grant revocation uses the same prepare-once, sign-once, submit-once, local-hash, prefund, absolute-deadline, and quarantine primitives; required revocation never silently self-funds and optional revocation cannot send unsponsored without a complete prefund/balance proof. Ambiguous and accepted/no-receipt revocation failures expose the canonical userOpHash on the failed result, event, and error. revokeGrant(..., { nonceSeed }) is a deliberate reconciliation override: use it only after querying that exact hash to prove whether it was included and then reading the canonical EntryPoint owner-sudo nonce. A timeout alone is not evidence that the operation failed; an incorrect seed can duplicate or skip a revocation. Omission never clears the process-local quarantine, and a seed for another logical nonce key is rejected.

Kernel version pinning

The adapter supports Kernel 0.3.1, 0.3.2, and 0.3.3; 0.3.3 remains the default. Kernel version participates in account address derivation, so existing fleets must pass the version they were created with everywhere it is accepted:

const kernelVersion = "0.3.2" as const;
const moesi = createMoesi(zerodevPreset({ kernelVersion }));
const session = await moesi.authorize({ owner, chains, policies });

The returned session carries the selected version so execution deserializes against the matching account.

Policy-native authorization

authorizePolicies accepts provider-neutral exact-action and erc20-transfer artifacts from moesi/settlement. Here, exact-action identifies the reviewed action; the per-constraint result—not the name—states which parts are enforced on-chain. The adapter compiles target, selector, supported argument rules, value, expiry, and configured transaction/rate limits into Kernel policies. The EOA compiler fails closed when a policy requires on-chain enforcement.

Ordinary CallPolicy sessions still report full calldata hashing as advisory. The scoped kernel-threshold-2-of-2 proposal and compileKernelThresholdExactAction can bind the Kernel sender, calldata, nonce, and final UserOperation digest through two proposal-specific signatures. encodeKernelThresholdSignatures emits the patched validator's required approval-then-UserOperation wire order. Either signature alone, a stale proposal, or a changed bound field fails. This is not standing multisig governance for the Kernel.

The composed authorization retains one reviewed authority in memory for a sequence of application phases. A session can review either a zero-value target/selector superset or full policy-native constraints, then execute only subsets of that snapshot without another enable signature. Policy sessions preserve argument, value, recipient, timestamp, rate/transaction, and aggregate-outflow guards; aggregate accounting remains enforced by the installed on-chain policies.

Sessions fail closed after expiry, revocation, Kernel/chain binding drift, or an owner-account change. Bare EIP-1193 providers are watched with eth_accounts automatically. Their connected chain must remain among the chains authorized by the session, while switching between reviewed chains is allowed because session execution itself uses chain RPCs rather than the owner wallet. Call revoke() for on-chain uninstall and dispose() to release the in-memory session authority. Session revocation requires the action's explicit permissionId and verifies that its uninstallValidation calldata encodes the same validation ID before any RPC, preventing one session from uninstalling a different grant.

This also supports an explicit authorize-now/execute-later flow. The owner signs ENABLE once; the retained session then reuses the same session key for actions that pass the send boundary's policy checks. matchesReviewedAuthority compares only chain, owner, and Kernel identity; it never claims that proposed actions are covered or that live on-chain authority remains installed:

const session = await moesi.authorize({
  owner,
  chains,
  policies,
});

const retained = session.getReviewedChain(chains[0].key);
if (
  !retained ||
  !session.matchesReviewedAuthority({
    chain: chains[0],
    ownerAddress,
    accountAddress: retained.accountAddress,
  })
) {
  throw new Error("The retained session belongs to another reviewed authority");
}

// No additional owner signature: the enabled session key signs these UserOps.
const first = await moesi.executeActions({ chain: chains[0], session });
const second = await moesi.executeActions({ chain: chains[1], session });

selectUserOperationExecutionMethod() exposes the automatic choice before execution. It is chain-neutral: callers report signer and transport availability instead of the selector branching on a chain ID. Signer and transaction sender are deliberately separate:

const method = selectUserOperationExecutionMethod({
  signMethod: "auto",             // "owner" | "session-key"
  transactionSender: "auto",      // "bundler" | "owner"
  hasActiveSession: session.status === "active",
  planFitsSingleUserOperation,
  ownerSignerAvailable: true,
  bundlerAvailable: true,
  ownerTransactionSenderAvailable: true,
});

Automatic signer selection first reuses an already-enabled session when the selected sender can carry a session-signed UserOperation. If the caller pins the owner transaction sender, or auto mode finds no bundler, the adapter selects an available owner signer and emits the reviewed sign-method-fallback warning before signing. Without an active session, auto selects the owner when the complete prepared action set fits one UserOperation; a multi-UserOperation plan selects a session key to avoid repeated owner prompts when a bundler is available. Automatic transaction submission is bundler-first. It may fall back to owner-sent EntryPoint.handleOps only after a conclusive rejection proving the bundler did not accept an owner-signed operation. A session-signed operation is never rerouted through the owner. Ambiguous transport failures and post-acceptance receipt failures are quarantined for reconciliation and never resubmitted through another sender. ownerSignerAvailable and ownerTransactionSenderAvailable are separate capabilities: the presence of an owner signer does not claim that a direct transaction transport exists on the selected chain, and an owner transport does not make a session signature eligible for owner submission.

Use non-auto values to pin either axis independently, not to request an arbitrary pair. signMethod: "session-key" plus transactionSender: "owner" fails closed because the owner-submitted EntryPoint path cannot safely submit a session-signed operation. A pinned bundler never falls back to the owner, a pinned owner sender skips the bundler, and neither hard pin is silently changed. The selection result contains distinct signer and sender reasons so UIs and audit logs do not confuse “owner signed the UserOperation” with “owner sent the transaction.”

compileZeroDevPolicies supports recipientMatch: "one-of" | "equal", an explicit any-recipient mode (recipients: []), and rateLimit.variant: "default" | "with-reset". These switches exist for permission-ID parity with already installed grants; inspect the compiled constraint evidence rather than inferring enforcement from the policy name.

Remote owner signers

The signer bridge subpath supplies concrete, dependency-free adapters for AWS KMS and Turnkey. Authentication, SDK clients, key identifiers, and credentials stay in the host application; Moesi receives only a narrow exact-digest callback and verifies every returned signature against the configured Ethereum address before use.

For AWS KMS, use an ECC_SECG_P256K1 key. The adapter fixes MessageType: "DIGEST" and SigningAlgorithm: "ECDSA_SHA_256", strictly parses the returned DER signature, normalizes low-s, derives recovery parity, and verifies the address:

import { SignCommand } from "@aws-sdk/client-kms";
import {
  awsKmsSignerToLocalAccount,
} from "@moesi/settle-zerodev/administration";

const owner = awsKmsSignerToLocalAccount({
  address: kmsEthereumAddress,
  keyId,
  sign: request => kms.send(new SignCommand(request)),
});

For Turnkey, the adapter requests hexadecimal ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2 semantics with HASH_FUNCTION_NO_OP, validates r/s/v, normalizes low-s, and verifies the same address:

import {
  turnkeySignerToLocalAccount,
} from "@moesi/settle-zerodev/administration";

const owner = turnkeySignerToLocalAccount({
  address: turnkeyEthereumAddress,
  signWith: turnkeyPrivateKeyId,
  signRawPayload: request =>
    turnkey.apiClient().signRawPayload(request),
});

Other HSMs and remote wallets can implement the smaller EthereumDigestSigner contract and use digestSignerToLocalAccount(). That generic callback must return a 65-byte recoverable Ethereum signature for the exact 32-byte digest; it is still recovered and address-checked locally.

Owner administration and activity

executeActionsAsOwner() is the high-level owner-sudo path. It keeps the same ordered packing, route selection, sponsorship, receipt, retry, event, and nonce-safety behavior while creating no session permission. The returned strategy and onStrategySelected callback make the authority choice visible before any signature. selectExecutionStrategy() remains the signer-only compatibility helper. New code should use selectUserOperationExecutionMethod() so signer and transaction sender remain separate. An active reviewed session wins in auto mode; otherwise a complete plan that fits one UserOperation may use the owner, while multi-operation plans that require multiple UserOperations prefer the reusable session path.

createOwnerKernelClient is the lower-level client over caller-provided ERC-4337 bundler and optional ERC-7677 paymaster transports. Configured fallback routes remain provider-neutral; ZeroDev routes retain their sponsorship method, and UltraRelay retains zero-fee behavior. Use it for custom owner administration when credentials stay behind an application proxy. It is not a bounded session-key grant. The same Kernel version pin and account index must match the existing account.

reconstructActivity scans confirmed block ranges and reconstructs ERC-20 and native outflows, attributing permission-validated UserOperations through their nonce-encoded permission ID. Pass the trusted ERC-20 contracts in tokens so the RPC query and the local decoder reject lookalike Transfer logs from untrusted contracts. Omitting tokens intentionally retains topic-only discovery, which can be spoofed by any contract; an empty list disables ERC-20 reconstruction. ERC-20 events expose their source logIndex, while native calldata-derived events use null.

Pair the scan with findDeployBlock from moesi/resolver; a null deploy block is inconclusive when archive history is unavailable, so applications need a visible lookback fallback.

Bundler errors

Owner-signed clients automatically convert failures from sendUserOperation and waitForUserOperationReceipt into structured MoesiError instances. For custom UserOperation flows, use the same public decoder directly:

import { explainBundlerError } from "@moesi/settle-zerodev/tooling";

try {
  await client.sendUserOperation(request);
} catch (error) {
  const { kind, problem, cause, fix } = explainBundlerError(error);
  if (kind === "unknown") {
    renderApplicationFallback(cause);
    return;
  }
  console.error({ kind, problem, cause, fix });
}

Pass { target, data } as the optional second argument when inner-call context is available. kind: "unknown" is the stable, documented fallback contract when no supported bundler signal was classified; branch on kind rather than matching the human-readable problem. Its default remediation is context-neutral so CLIs and web applications can supply their own workflow. AA_ERROR_TABLE and flattenErrorMessages are also public for consumers that need custom presentation. Diagnostics stay redacted by default. If the failing route is your own trusted backend, recover its raw REST response before redaction at that explicit trust boundary:

import { extractHttpErrorBody } from "@moesi/settle-zerodev/tooling";

const response = extractHttpErrorBody(error);
// { status: 409, body: '{"error":"actionable first-party message"}' }

response.body is intentionally unredacted; do not log or publish it for third-party bundler/paymaster transports.

Custom-chain settlement

Custom entries may supply a BYO ERC-4337 bundler, sponsorship mode, and an independent paymaster. Use self-funded for a funded Kernel, required when a missing sponsor must block, or optional only when balance/prefund proof may permit fallback. Tier B can submit an already signed UserOperation directly through EntryPoint handleOps, so the operator EOA fronts gas without receiving policy authority. submitUserOperationViaEntryPoint() reports success only when the canonical transaction receipt contains exactly one matching, successful EntryPoint v0.7 UserOperationEvent for the computed hash, sender, and nonce. Outer reverts, inner failures, and missing, duplicate, or mismatched event evidence reject.

getReferenceKernelBootstrapManifest() and bootstrapKernelStack() implement the Tier-C virgin-chain path. They deploy and byte-attest Nick's deterministic deployment proxy, CreateX, EntryPoint 0.7 and its SenderCreator, and the supported Kernel v3 factories, validators, policies, signers, hooks, and helpers, assembled from the self-contained per-contract manifests in moesi/registry. EntryPoint 0.6 and Kernel v2-era variants are deliberately omitted. Moesi's patched WeightedValidator is retained as an explicit extension. The preflight examines every supported canonical address before the first transaction; an unexpected predecessor fails closed, while a predecessor matching the reviewed runtime evidence can be retained. EIP-712 runtime hashes are derived for the target chain ID rather than copied from Ethereum. For CREATE2 components, manifest validation binds each deploy payload to its canonical address before RPC. Arbitrary constructor execution means the resulting runtime cannot be established statically, so each transaction is awaited and its runtime is byte-attested before bootstrap continues or reports success.

License

Apache-2.0