ERC-4337
The UnsignedUserOp shape, the x-client-signed contract convention, and how the backend treats the bundler.
Every state-changing action on a Botanary account ends up as an ERC-4337 UserOperation. The backend builds it, the owner (or an authorized device or session key) signs it, and the backend relays what was signed - never the other way around.
UnsignedUserOp
The contract's core building block is UnsignedUserOp: "ERC-4337 (EntryPoint v0.7) UserOp with an
empty signature. The owner signs userOpHash (returned alongside in the build envelope) client-side...
The backend builds and later relays it - it never signs."
Required fields: sender, nonce, callData, callGasLimit, verificationGasLimit,
preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, signature, entryPoint, chainId.
Optional fields cover account deployment and gas sponsorship: factory, factoryData, paymaster,
paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData.
The x-client-signed convention
Any schema or endpoint whose payload the owner has to sign before it's usable carries an
x-client-signed: true marker in the contract - it's on UnsignedUserOp, GasPermit, UserOpBuild,
SignedUserOp, and on every build endpoint that returns one of them (POST /account,
POST /accounts, POST /account/freeze, POST /userops, and more). If you're generating tooling
against this contract, that marker is a reliable way to find every payload that needs a client-side
signature before it can be used.
SignedUserOp and UserOpReceipt
Once the owner (or delegate) has signed, the client wraps the result as a SignedUserOp:
{ userOp: UnsignedUserOp, userOpHash, intentType } - "a previously built op with the owner's
client-side signature attached, for relay." Submitting it returns a UserOpReceipt:
{ id, userOpHash, status, txHash (nullable), submittedAt, error (nullable) }, which you poll until
it reaches a terminal status. See User operations for the full
relay and polling lifecycle.
The bundler is availability, not authority
The backend talks to the ERC-4337 bundler through a port interface, not a hardcoded vendor - a code comment on that port is explicit about the trust boundary: "The ERC-4337 bundler (ZeroDev/Pimlico). NOT trusted for authority." The bundler is what gets a signed operation onto the chain; it can't change what that operation is authorized to do, and neither can the backend relaying it.
Local development runs the bundler-facing ports against fake adapters entirely - see Local development for the anvil-based stack, which doesn't depend on a real bundler or paymaster vendor at all. Where a real vendor is wired up, the relevant configuration lives in a small set of environment variables scoped to bundler and paymaster RPC endpoints and project identifiers - variable names only, never checked into any published artifact.
Whichever vendor sits behind the port, the contract's own framing holds: relaying "is availability, never authority - the server cannot alter the signed op's effect." Everything that actually governs what an operation is allowed to do is enforced in ERC-4337 validation and in AgentGuard's execution hook - see ERC-7579 modules.