Secure crypto wallet skill for OpenClaw agents. Hardware-isolated keys (Apple Secure Enclave), on-chain spending caps, ERC-4337 smart wallet.
See docs/SPEC.md for the full technical specification (read it when you need architecture/security details, don't load at startup).
- Daemon: Swift, macOS only. CryptoKit (Secure Enclave P-256), Foundation (Unix socket server). Runs as LaunchAgent with Mach service for XPC.
- Companion: SwiftUI menu bar app (
LSUIElement). LAContext (Touch ID) + UNUserNotificationCenter. Connects to daemon via bidirectional XPC. - Skill: Node.js scripts +
SKILL.md. Standard OpenClaw skill structure. Communicates with daemon over Unix socket. - Contracts: Solidity 0.8.x. Coinbase Smart Wallet fork + policy module. Foundry for build/test. Targets ERC-4337 v0.7 EntryPoint.
- Chains: Ethereum L1 (chainId 1) and Base (chainId 8453) only.
- Bundler: Pimlico public endpoints, no API key.
https://public.pimlico.io/v2/{chainId}/rpc
daemon/ Swift signing daemon (macOS LaunchAgent + Mach service)
companion/ SwiftUI menu bar app (Touch ID, notifications, approvals)
shared/ Shared XPC protocols (symlinked into both targets)
skill/ OpenClaw skill (SKILL.md + Node scripts)
contracts/ Solidity smart wallet + policy + recovery
src/
test/
script/
scripts/ Dev tooling (codesign-dev.sh)
docs/ Architecture docs
SPEC.md Full technical specification
cd contracts && forge build— compile contractscd contracts && forge test— run contract testscd contracts && forge test -vvv --match-test <name>— verbose single testcd daemon && swift build— build daemoncd daemon && swift test— run daemon unit testscd daemon && swift build && ../scripts/codesign-dev.sh— build + ad-hoc sign for SE accesscd companion && swift build— build companion app
- Skill is untrusted. The skill MUST only emit
{target, calldata, value}. It MUST NOT set nonce, gas, chainId, fees, or signatures. The daemon owns UserOp construction exclusively. - Default-deny policy. Unknown calldata → require human approval. Policy is an allowlist, not a blocklist. When in doubt, require approval.
- No paymasters.
paymasterAndDatais always empty. User pays gas. Daemon must preflight every tx (check ETH balance ≥ estimated cost + buffer) and refuse if insufficient. - Two Secure Enclave keys. Signing key: no
.userPresence(signs silently for routine ops). Admin key:.userPresencerequired. Touch ID prompted by companion app (LAContext), never by daemon. If companion unreachable, admin actions fail closed (503). - On-chain wallet verifies standard
userOpHashonly. Never the reduced ApprovalHash. See SPEC.md §7.1. - Blocked selectors.
approve,increaseAllowance,decreaseAllowance,setApprovalForAll,permit(EIP-2612 + DAI), Permit2 — all MUST require explicit user approval regardless of context. - Low-S normalization. Every P-256 signature must be normalized: if
s > n/2, replace withn - s. Contracts may reject high-S. - Panic asymmetry.
freeze()is instant, no Touch ID. Unfreeze requires Touch ID + 10min delay. Key rotation auto-freezes + 48h delay. - Freeze integrity. On-chain
frozenforces localfrozen(one-way, 60s sync + startup check before serving). Never auto-unfreeze. - Approval code isolation. Codes never exposed via Unix socket API. Shared with companion only over verified XPC (code-signing check).
- Config integrity. Config signed with SE key. Tampered/malformed config → safe mode (frozen).
- Socket:
~/.monolith/daemon.sock(0600, directory 0700) - Auth: peer UID check only (same OS user). No HMAC, no shared secrets.
- Admin actions (
/policy/update,/allowlist,/unfreeze): daemon sends XPC callback to companion → companion shows SwiftUI sheet + Touch ID → result returned. Companion not connected → 503.
| Endpoint | Method | Auth | Notes |
|---|---|---|---|
/sign |
POST | Socket | Policy-checked, gas-preflighted |
/decode |
POST | Socket | Human-readable intent summary, no signing |
/capabilities |
GET | Socket | Limits, budgets, gas status ("ok"/"low") |
/policy/update |
POST | Socket + Companion approval | Companion shows confirmation + Touch ID |
/allowlist |
POST | Socket + Companion approval | Companion shows confirmation + Touch ID |
/unfreeze |
POST | Socket + Companion approval | Verifies on-chain unfrozen first |
/panic |
POST | Socket | Instant freeze, no Touch ID |
/health |
GET | None | Status check |
- Fork of Coinbase Smart Wallet. P-256 sig verification in
validateUserOp. validateUserOpchecksfrozenfirst — rejects all UserOps when frozen (prevents wasted gas).- Use precompile at
0x100when available (EIP-7951 L1, RIP-7212 Base), fallback to Daimop256-verifier. - On-chain daily spending cap tracks both native ETH and known stablecoins.
- Recovery: single
recoveryAddresswithfreeze()(instant) +initiateKeyRotation()(48h timelock, auto-freezes).
Two built-in profiles configured at setup. Limits are identical across chains.
- Balanced: 100 USDC/tx, 500/day, 0.05 ETH/tx, 0.25/day, 10 tx/hr, 1% max slippage. Protocols: Uniswap + Aave.
- Autonomous: 250 USDC/tx, 2000/day, 0.15 ETH/tx, 0.75/day, 30 tx/hr, 2% max slippage. Protocols: Uniswap + Aave + Aerodrome (Base) / Lido + Rocket Pool (L1, ETH-in only).
- Swift: Swift 5.9+, async/await, structured concurrency. No force unwraps in production code. All Secure Enclave operations wrapped in do/catch.
- Solidity: Foundry conventions. NatSpec on all public functions. Custom errors over require strings. Events for all state changes.
- Node/Skill: ESM modules, no TypeScript (keep skill minimal). Error handling must surface daemon errors clearly to the agent.
- Precompile at
0x100must be probed at runtime with 3 test vectors (valid sig, invalid sig, malformed input) — never assume from docs. - Pimlico public endpoint is IP-rate-limited. Implement exponential backoff on 429s. No tight polling loops.
- Stablecoins identified by
(chainId, contractAddress)tuple, never by symbol string. - ERC-8004 identity registers on the home chain (not forced to L1). Wallet must exist on that chain for ERC-1271 verification.
- Signature format is raw
r||s(64 bytes). No DER encoding. - Bundler reimburses itself from wallet balance atomically. If wallet has insufficient ETH, the UserOp will revert on-chain and the user still loses gas. That's why preflight is critical.
- Codesign after build: Every
swift buildstrips signature. Runscripts/codesign-dev.shafter building for SE access. - Companion required for admin/approval:
/policy/update,/allowlist,/unfreezeneed companion running./signrequiring approval also needs companion. Routine/signwithin policy works without companion. - On-chain freeze sync is one-way: On-chain freeze forces local freeze. Local unfreeze requires explicit
/unfreezeafter on-chain unfreeze. - Config edits trigger safe mode: Manual edits to
~/.monolith/config.jsoninvalidate the SE signature → daemon starts frozen. - LaunchAgent plist required for XPC:
com.monolith.daemon.plistmust be in~/Library/LaunchAgents/withMachServicesdictionary for XPC to work.