Skip to main content
A debate produces a Plan. apply ships it. Between those two words sits the execution pipeline: a deterministic gate that builds calldata, runs a simulation, enforces a policy allowlist, and hands the transaction to Turnkey for signing. The user never sees a wallet popup because there isn’t one to show. The CLI never holds a signing key. This page traces what happens between the moment the user types apply and the moment the transaction lands on chain.

The user-facing flow

Six commands; one of them types a transaction onto chain. The rest are inspection and consent.

What happens on apply

Concretely, applyPlan (in packages/runtime/src/tools/plan/index.ts) runs five gates in order. Any gate failing aborts the apply before Turnkey sees the request.

1. Approval state

The plan must be explicitly approved by the user via approve it. This is a state flag on the session; no signing happens at approval time.

2. Deterministic transaction build

buildExecutionTransaction (in packages/execution/src/plans/transaction.ts) builds the calldata from the plan’s tick math, not from anything the LLM produced:
The agents pick (widthMultiplier, centerOffsetTicks). The execution layer turns those into bytes.

3. On-chain simulation

simulatePlan runs an eth_call-style preflight against the live chain state. Catches reverts before any signing happens. Estimates gas, slippage, and the post-trade range.

4. Policy gate

checkExecutionPolicy (in packages/execution/src/plans/policy.ts) enforces a hard allowlist before the transaction is allowed to leave the runtime: If any of these fail, the apply returns POLICY_REJECTED with the reasons. Nothing reaches Turnkey.

5. Turnkey sign + submit

Only if every gate passes does the runtime call:
signAndSubmit (in packages/chain/src/wallet/turnkey/client.ts) constructs a user-scoped Turnkey client using the session’s ephemeral keypair, then calls Turnkey’s ethSendTransaction. Turnkey verifies the request, signs with the user’s sub-organization wallet, and submits to the chain. The runtime gets back transactionHash and turnkeyActivityId (Turnkey’s audit trail id).

Sign-in and session lifecycle

The signing setup happens once per hour, not per transaction.
  1. User types login <email>. CLI calls Turnkey initOtp (directly for self-hosted; via apps/proxy for the npm-published binary).
  2. Email arrives. User enters the OTP.
  3. CLI generates a fresh P256 ephemeral keypair locally (generateP256KeyPair), hands the public key to Turnkey via otpLogin. Turnkey returns scoped session credentials (apiPublicKey, apiPrivateKey) bound to that ephemeral keypair.
  4. First-time sign-in provisions a Turnkey sub-organization with its own wallet address. The wallet’s signing key lives inside Turnkey’s secure enclave; the parent organization has read-only access by Turnkey’s architecture.
  5. Session is persisted to ~/.zuno/session.json with mode 0600 and a 1-hour TTL.
The CLI holds session API credentials. It does not hold the wallet’s signing key, ever.

Security properties at a glance

What ships back to the user

apply returns a structured result the CLI renders as a confirmation panel: plan id, position id, pair, fee tier, old and new range, gas estimate, transaction hash, and the Turnkey activity id (so the user can audit the request in their Turnkey dashboard).

Where the code lives

  • packages/runtime/src/tools/plan/index.ts - applyPlan tool
  • packages/execution/src/plans/apply.ts - prepareApply pipeline
  • packages/execution/src/plans/transaction.ts - calldata build
  • packages/execution/src/plans/policy.ts - policy allowlist
  • packages/execution/src/plans/simulation.ts - on-chain preflight
  • packages/chain/src/wallet/turnkey/client.ts - signAndSubmit
  • packages/chain/src/wallet/turnkey/auth.ts - OTP, session keys