Skip to content

Decouple UserOp submission from the broker into an ERC-4337 bundler #230

@hanwencheng

Description

@hanwencheng

Context

The broker's /v1/accept/submit (crates/agentkeys-broker-server/src/handlers/accept.rs) submits the K11-signed accept UserOp by shelling out to cast send EntryPoint.handleOps(...). So the broker IS the bundler — it collapses three ERC-4337 roles (auth/policy, paymaster co-sign, and tx submission) into one service, and drags a foundry/cast runtime dependency into the broker's systemd unit. That already bit us: 502 {"error":"spawn cast: No such file or directory"} — the unit runs ProtectHome=true, so a ~/.foundry/bin cast is unreachable (worked around in 8b3715f with AGENTKEYS_CAST_BIN + copying cast to /usr/local/bin, but that's a band-aid over the coupling).

Canonical ERC-4337 keeps submission behind a dedicated bundler: the broker hands a signed UserOp to the bundler via eth_sendUserOperation, and the bundler batches + submits handleOps, owning the beneficiary EOA, nonce, gas, and resubmission. The broker keeps only the policy it should own — J1 auth + the VerifyingPaymaster co-sign (the Sybil gate).

Proposed architecture

Submit flow:

sequenceDiagram
  autonumber
  participant C as Client (browser, K11 passkey)
  participant B as Broker (auth + paymaster co-sign)
  participant U as Bundler (NEW service)
  participant E as EntryPoint
  participant P as VerifyingPaymaster
  Note over C,B: build — sponsorship (unchanged)
  C->>B: POST /v1/accept/build (J1_master)
  B->>B: assemble executeBatch UserOp + co-sign paymaster getHash
  B-->>C: userOpHash + paymasterAndData
  Note over C: Touch ID over userOpHash (K11)
  Note over C,U: submit — DECOUPLED
  C->>B: POST /v1/accept/submit (signed UserOp)
  B->>U: eth_sendUserOperation(userOp, EntryPoint)
  U->>E: handleOps([userOp], beneficiary)
  E->>P: validatePaymasterUserOp (verify broker co-sign)
  E->>E: executeBatch([registerAgentDevice, setScope])
  U-->>B: userOpHash / receipt
  B-->>C: { tx_hash }
Loading

Roles (proposed):

flowchart LR
  C[Client] -->|build + submit, J1| B[Broker: auth, cap-mint, paymaster co-sign]
  B -->|EIP-191 co-sign| P[VerifyingPaymaster]
  B -->|eth_sendUserOperation| U[Bundler: batch, nonce, gas, resubmit]
  U -->|handleOps| E[EntryPoint]
  E --> P
  E --> A[P256Account: validateUserOp + executeBatch]
Loading

The bundler is swappable behind one eth_sendUserOperation RPC: self-hosted eth-infinitism bundler, a thin in-house Rust submitter, or a 3rd-party (Pimlico / Alchemy / Stackup). There's a scripts/erc4337-bundler.sh stub to build on.

Scope

  • Replace accept_submit's cast send handleOps with an eth_sendUserOperation relay to a configurable bundler URL (AGENTKEYS_BUNDLER_URL); drop the broker's cast/foundry dependency + the AGENTKEYS_CAST_BIN workaround.
  • Stand up / wire a bundler service (start with self-hosted eth-infinitism or the erc4337-bundler.sh stub) into setup-broker-host.sh (or its own host), incl. the beneficiary EOA + EntryPoint deposit funding.
  • Keep the broker's build-side paymaster co-sign unchanged (the J1 Sybil gate).
  • Poll the bundler for the receipt (eth_getUserOperationReceipt) and return tx_hash to the client.

Acceptance

  • accept_submit no longer spawns cast; the broker unit needs no foundry.
  • A web accept (one Touch ID) lands executeBatch([registerAgentDevice, setScope]) on-chain via the bundler, sponsored by the VerifyingPaymaster.
  • The bundler URL is configurable; swapping self-hosted ↔ 3rd-party needs no broker code change.
  • The diagram above reflects the shipped flow; runbook + arch.md updated.

Effort

~L. The relay + receipt-poll is small; standing up + funding a reliable bundler service (or integrating a 3rd-party) is the bulk. Heima must accept the bundler's handleOps gas model (legacy tx, no prevrandao — see CLAUDE.md "Heima EVM compatibility").

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/brokerBroker server, cap-token issuance, OIDC issuance

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions