# Smart wallet user operations

Source: https://docs.settlemint.com/docs/api-reference/guides/user-operations
Estimate, send, replace and track ERC-4337 user operations for a smart wallet through the Platform API or the bundler JSON-RPC endpoint.

A user operation is the signed instruction your client sends for a smart wallet in place of a raw transaction, as the ERC-4337 account-abstraction standard defines it. Most integrations never call these routes: an ordinary platform write from a smart wallet makes the platform build and bundle the user operation itself, as [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) describes. Use these routes when your client builds and signs user operations itself. A bundler is the service that packs user operations into one transaction and submits it. The platform runs the bundler for your organisation, so you do not run one yourself. You estimate gas, sign the operation and send it. The platform then bundles it, pays for the bundle from the organisation's bundler wallet and records the outcome.

Two interfaces reach the same bundler: the REST routes under `/api/v2/bundler/{chainId}` and the ERC-4337 JSON-RPC endpoint at `/bundler/{chainId}`. `chainId` is the decimal chain ID of the network.

Every read is scoped to the organisation and the network in the path. An operation stored for another organisation or another network reads as `null`.

The examples on this page read `DALP_URL` as your instance's base URL, `CHAIN_ID` as the network's decimal chain ID and `DALP_API_KEY` as a read-write key.

## Before you start [#before-you-start]

- An [API key](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) in the `X-Api-Key` header, for a member of an active organisation. The estimate, send and paymaster routes are declared as writes, so a `read` key receives `DALP-0036` and they need a `read-write` key. The `GET` reads accept either scope.
- A smart wallet the organisation can use on the network. The estimate, send and paymaster routes refuse a sender outside the organisation.
- A network the organisation can use. Its default network qualifies when it is an enabled EVM network. Another network also needs multi-network access and an active deployment on that network. Every route, REST and JSON-RPC, refuses any other network.
- The EntryPoint address for the network, which `GET /api/v2/bundler/{chainId}/supported-entry-points` returns.

## Read the EntryPoint [#read-the-entrypoint]

The EntryPoint is the shared ERC-4337 contract that validates and executes each user operation. Each network has one, and the signature is collected over it, so read it before you prepare an operation.

```bash
curl "$DALP_URL/api/v2/bundler/$CHAIN_ID/supported-entry-points" \
  --header "x-api-key: $DALP_API_KEY"
```

The response is an array with exactly one address. This address is illustrative:

```json
["0x2222222222222222222222222222222222222222"]
```

Submission accepts only that address and refuses any other. Gas estimation and paymaster data also accept the network's configured Directory contract, the platform's registry of system contracts on the network, in its place, and refuse any other address.

## Estimate gas before signing [#estimate-gas-before-signing]

`POST /api/v2/bundler/{chainId}/estimate` takes the EntryPoint and the unsigned operation. The platform simulates the operation and returns the gas limits to put into it before you sign. In the example, `callData` is a placeholder: replace it with the encoded call your smart wallet executes.

```bash
curl --request POST "$DALP_URL/api/v2/bundler/$CHAIN_ID/estimate" \
  --header "x-api-key: $DALP_API_KEY" \
  --header "content-type: application/json" \
  --data '{
    "entryPoint": "0x2222222222222222222222222222222222222222",
    "userOperation": {
      "sender": "0x1111111111111111111111111111111111111111",
      "nonce": "0x0",
      "callData": "0x<encoded call>"
    }
  }'
```

The response holds three gas values. For an operation that names a paymaster, a contract that pays gas for the sender, it also holds `paymasterVerificationGasLimit` and `paymasterPostOpGasLimit`. These numbers are illustrative:

```json
{
  "preVerificationGas": "0x1d4c0",
  "verificationGasLimit": "0x30d40",
  "callGasLimit": "0x186a0"
}
```

The platform simulates the call at the call gas limit it returns, so the call finishes within that limit. A call that does not fit inside one transaction on the network is refused. Over JSON-RPC the message names the gas the call needs and the gas one transaction leaves for it.

The returned `preVerificationGas` is the price `send` checks the signed operation against, so sign that value or a higher one. The price counts the signature, the paymaster data and the fixed-width nonce, gas limit and fee fields at the non-zero rate whatever they hold, so it depends only on the sender, the `initCode`, the `callData` and the lengths of the paymaster data and the signature. A stub signature priced at estimation costs the same as the real signature of the same length, and a nonce, fee or gas limit that changes after the estimate does not move the price. Estimate with a stub as long as the signature you will send, because a longer signature raises the price.

Quantity fields accept a decimal string, a hexadecimal quantity or a non-negative integer. The examples on this page use hexadecimal quantities. The platform reads the upper 192 bits of `nonce` as the nonce key and the lower 64 bits as the sequence within that key.

## Sign the operation [#sign-the-operation]

The platform identifies an operation by its ERC-4337 hash: the EIP-712 hash of the packed operation under the domain name `ERC4337`, version `1`, the network's chain ID and the EntryPoint address. Your client computes this hash itself and signs it before it calls `send`, which returns the same hash. A signature over any other EntryPoint does not match it. The signature format is the one the smart wallet's validator expects, which [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) describes.

## Send a signed operation [#send-a-signed-operation]

`POST /api/v2/bundler/{chainId}/send` takes the EntryPoint and the signed operation. The operation must carry `callGasLimit`, `verificationGasLimit`, `preVerificationGas`, `maxFeePerGas`, `maxPriorityFeePerGas` and `signature`. A request without one of them is refused. The values in this example are illustrative, and a real signature is longer.

```bash
curl --request POST "$DALP_URL/api/v2/bundler/$CHAIN_ID/send" \
  --header "x-api-key: $DALP_API_KEY" \
  --header "content-type: application/json" \
  --data '{
    "entryPoint": "0x2222222222222222222222222222222222222222",
    "userOperation": {
      "sender": "0x1111111111111111111111111111111111111111",
      "nonce": "0x0",
      "callData": "0x<encoded call>",
      "callGasLimit": "0x186a0",
      "verificationGasLimit": "0x30d40",
      "preVerificationGas": "0x1d4c0",
      "maxFeePerGas": "0x3b9aca00",
      "maxPriorityFeePerGas": "0x5f5e100",
      "signature": "0x4f2a"
    }
  }'
```

The response is the user operation hash:

```json
"0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b"
```

The platform stores the operation and answers with its hash before any bundle exists. Keep the hash for the status and receipt reads. Before it stores the operation, the platform runs these checks in order:

1. The priority fee must reach the deployment's configured minimum. A lower fee is refused. A network whose fee quote is zero for both fee components skips this check.
2. `maxFeePerGas` must reach 1.5 times the base fee of the network's latest block, zero on a network whose blocks carry none, plus the operation's own `maxPriorityFeePerGas`, the same headroom the bundler signs its bundle with. A lower value is refused with `UserOperation maxFeePerGas <signed> is below <floor>, 1.5 times the chain's base fee plus the operation's maxPriorityFeePerGas`, or, on a network whose blocks carry no base fee, `UserOperation maxFeePerGas <signed> is below its own maxPriorityFeePerGas <tip>`. An operation that signs zero for both fee fields on a network whose fee quote is zero for both fee components skips this check and the next one.
3. `preVerificationGas` must reach the gas the bundler pays to carry the operation, the price the estimate returns. The price is 21,000 plus the larger of two sums, over the operation's ABI-encoded bytes with the signature, the paymaster data and the fixed-width nonce, gas limit and fee fields counted as non-zero. The first is 85,800 plus those bytes at 4 gas a zero byte and 16 a non-zero byte, plus 10 gas a 32-byte word. The second is the network's calldata floor over those bytes plus the 132 bytes of the `handleOps` call around them, priced at the floor's non-zero rate. From the Prague fork that floor is 10 gas a zero byte and 40 a non-zero byte (EIP-7623). On the Amsterdam fork it is 64 gas a byte (EIP-7976). A lower value is refused with `UserOperation preVerificationGas <signed> is below <price>, the gas the bundler pays to carry it on this chain`. Both messages print each value as a hexadecimal quantity followed by its decimal value.
4. On a network whose blocks carry a calldata floor, the operation's packed, ABI-encoded size must not exceed the deployment's limit, 8,192 bytes by default, the ERC-7562 limit. A larger operation is refused with `UserOperation is <size> bytes ABI-encoded, above the <limit> bytes the bundler accepts on a chain that charges a calldata floor`. An operation exempt from steps 2 and 3 skips this check too.
5. Validation is simulated with the operation's own paymaster fields, so a sponsored operation is checked as sponsored. A failed simulation is refused, and so is an operation whose account or paymaster keeps it valid for less than 60 seconds from now (`validUntil`): the bundler could not include it in time.
6. The payer must hold enough funds for the prefund: the sum of the operation's gas limits multiplied by its `maxFeePerGas`. A self-paid operation counts the sender's native balance plus its deposit at the EntryPoint. A sponsored operation counts the paymaster's deposit at the EntryPoint. The check adds this operation's prefund to the prefund of the payer's other operations still in flight. A sender that falls short is refused with `AA21`, and a paymaster with `AA31`. To see the shortfall, read the sender's balance and its deposit at the EntryPoint, or the paymaster's deposit. The check is skipped on a network that charges no gas, for an operation whose prefund is zero, and when the platform cannot read the payer's funds.

Two limits answer `429` with code `BUNDLER_ADMISSION_THROTTLED` (`DALP-14439`), or JSON-RPC error `-32504`. Both are worth retrying unchanged:

| `limit` | When | Retry |
| --- | --- | --- |
| `sender-penalty` | A bundle reverted on chain because an operation from this sender became invalid after the platform admitted it, for example because the sender spent the nonce or its funds elsewhere. Checked before the steps above, for every new operation from the sender. | After `retryAfterSeconds`. The window is 24 hours by default. |
| `pending-operations` | The organisation already has the deployment's limit of operations sent through these routes waiting on this network, 256 by default. Checked when the operation is stored. A replacement is not a new operation and is never refused for it. | Once some of the organisation's pending operations have been bundled. |

A sender refusal applies only on the organisation and network whose bundle reverted, and only to the sender of the operation the revert names. The other senders in that bundle are not refused, and their operations go back to waiting. The sender's own other waiting operations fail with an error that names the refusal. Only a revert that names the account's own validation (`AA21`, `AA23`, `AA24`), a nonce another operation already used (`AA25`), or a paymaster the platform does not run (`AA31`, `AA33`, `AA34`) refuses the sender. A revert on the platform's own paymaster refuses no one. To stay clear of it, keep a sender's nonce, balance and EntryPoint deposit unchanged while its operations wait, and do not send the same operation to another bundler.

An operation that signs zero for both fee fields on a network whose fee quote is zero for both fee components skips checks 2, 3 and 4 while the network's latest block carries no base fee. When that block charges a base fee, the operation is refused with `UserOperation signs no fee, but the chain charges a base fee of <fee>`, because no bundle can carry it.

When the bundler builds a bundle, it runs checks 2 and 3 again on each signed operation, against the block it builds on, because the base fee or the network's fork can change after `send` stored the operation:

- An operation whose `maxFeePerGas` is below 1.5 times that block's base fee plus its own `maxPriorityFeePerGas` stays `PENDING` and is checked again at every new block. It joins a bundle once the base fee falls far enough for its `maxFeePerGas` to reach that floor. Later operations from the same sender and nonce key wait behind it. To send it sooner, [replace it](#replace-a-pending-operation) with higher fees. An operation still below the floor an hour after the platform last stored it (when it was sent, replaced, released from `BLOCKED`, or returned by a dropped bundle) becomes `FAILED`.
- An operation whose `preVerificationGas` is below the price at that block can never be carried, for example one stored before the network moved to a fork with a higher calldata floor. It becomes `FAILED`.
- An operation whose `maxFeePerGas` is below its own `maxPriorityFeePerGas` cannot pay at any base fee, and an operation that signs zero for both fee fields cannot be carried on a network whose blocks charge a base fee. Both become `FAILED`.

Each of these failures reads with `error` set to `PRE_FLIGHT_REJECTED`, the code the bundler also uses for an operation it refuses for its size or for a failed bundle simulation. Send a new operation with the estimate's `preVerificationGas` and fees that reach the current floor. An operation that signs zero for both fee fields on a network whose blocks carry no base fee skips the `preVerificationGas` check, because a bundle that carries it is filed at zero fees. Operations the platform prepares itself are priced when they are prepared and are not checked again.

Sending the same signed operation again returns the same hash and stores nothing new. When the stored operation is `PENDING`, the repeat hands it to the bundler again, and the bundler checks it as above. In every other state the repeat returns the hash and changes nothing, so a retry after a lost response is safe. A repeat does not resubmit an operation that failed. Send a new operation with a new signature instead.

## Replace a pending operation [#replace-a-pending-operation]

To reprice an operation that is waiting, send a new signed operation from the same sender with the same nonce to the same `send` route. Both `maxFeePerGas` and `maxPriorityFeePerGas` must reach the original value multiplied by 1.1, rounded down to a whole wei. A successful replacement swaps the stored operation for the new one, and the original hash then reads as `null`.

The platform refuses a replacement in three cases. The JSON-RPC message names each one:

| Case | JSON-RPC message |
| --- | --- |
| The original is already in a bundle. | `A UserOperation for this sender and nonce is already included in a bundle, so it can no longer be replaced` |
| A fee is below 1.1 times the original. | `A replacement UserOperation must raise both maxFeePerGas and maxPriorityFeePerGas by at least 10%` |
| The original was not sent through these routes, or it is `BLOCKED`. | `A UserOperation is already pending for this sender and nonce and cannot be replaced` |

The original's prefund does not count against the payer's funds when the platform checks the replacement.

## Read the status [#read-the-status]

`GET /api/v2/bundler/{chainId}/user-operations/{userOperationHash}` reads the stored operation.

```bash
curl "$DALP_URL/api/v2/bundler/$CHAIN_ID/user-operations/$USER_OPERATION_HASH" \
  --header "x-api-key: $DALP_API_KEY"
```

This response is illustrative. `transactionHash`, `blockNumber` and `error` appear only when the platform has recorded them. In this response `blockNumber`, when present, is a JSON number, while the receipt carries it as a hexadecimal quantity.

```json
{
  "id": "0192f6c0-7b4d-7f2a-9c6e-3f1a2b3c4d5e",
  "userOpHash": "0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b",
  "sender": "0x1111111111111111111111111111111111111111",
  "nonce": "0x0",
  "nonceKey": "0x0",
  "callData": "0x<encoded call>",
  "status": "BUNDLED",
  "createdAt": 1794000000000,
  "updatedAt": 1794000004000,
  "idempotencyKey": "0192f6c0-7b4d-7f2a-9c6e-3f1a2b3c4d5e",
  "transactionHash": "0x3a9e7c1d5b2f4a6e8c0d2b4f6a8e0c2d4b6f8a0e2c4d6b8f0a2e4c6d8b0f2a4e"
}
```

| Status | Meaning |
| --- | --- |
| `PENDING` | The platform has stored the operation and it is not in a bundle. |
| `BLOCKED` | The operation waits, for example on an approval or on funds. |
| `BUNDLED` | The operation is in a bundle the platform has built. The bundle may still be on its way to the network. |
| `MINED` | The bundle is on chain. The operation either succeeded or reverted, and the receipt tells you which. |
| `FAILED` | The operation does not execute. `error` holds the reason when the platform recorded one. |

Stop polling at `MINED` or `FAILED`. The schema in the API explorer also lists `PREPARING`, `SUBMITTING`, `CONFIRMED` and `EXPIRED`, which this route does not return. A `MINED` operation whose call reverted carries the revert data in `error`. The platform does not re-sign it with more gas, so send a new operation instead.

An operation can stay `BUNDLED` while an earlier transaction of the bundler wallet is missing, because the chain mines the wallet's transactions in order. The platform fills a skipped nonce itself with a zero-value transfer from the wallet to itself, so a skipped nonce does not block later bundles permanently. No call on your side is needed, and resending the operation does not speed it up. See [what happens to a nonce the bundler skipped](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations#what-happens-to-a-nonce-the-bundler-skipped).

## Read the receipt [#read-the-receipt]

`GET /api/v2/bundler/{chainId}/user-operations/{userOperationHash}/receipt` returns the on-chain outcome.

```bash
curl "$DALP_URL/api/v2/bundler/$CHAIN_ID/user-operations/$USER_OPERATION_HASH/receipt" \
  --header "x-api-key: $DALP_API_KEY"
```

The response is `null` until the operation has settled on chain as confirmed or reverted. It is also `null` for a hash the organisation does not hold on that network. This response is illustrative:

```json
{
  "userOpHash": "0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b",
  "sender": "0x1111111111111111111111111111111111111111",
  "nonce": "0x0",
  "success": true,
  "transactionHash": "0x3a9e7c1d5b2f4a6e8c0d2b4f6a8e0c2d4b6f8a0e2c4d6b8f0a2e4c6d8b0f2a4e",
  "blockHash": "0x5d7f9b1e3c5a7d9f1b3e5c7a9d1f3b5e7c9a1d3f5b7e9c1a3d5f7b9e1c3a5d7f",
  "blockNumber": "0x1a2b3c"
}
```

`success` is `false` when the call reverted, and `reason` then carries the revert data. Your operation succeeded when the receipt is present and `success` is `true`.

## Use the ERC-4337 JSON-RPC endpoint [#use-the-erc-4337-json-rpc-endpoint]

`POST /bundler/{chainId}` speaks the ERC-4337 bundler protocol. It takes the same `X-Api-Key` header and applies the same sender, EntryPoint and network rules as the REST routes. Every method needs a read-write key, the reads and `eth_chainId` included: the endpoint refuses a `read` key with HTTP 403, a JSON-RPC error whose `data.id` is `DALP-0036`, and `data.retryable` set to `false`.

It supports these methods:

- `eth_chainId`
- `eth_supportedEntryPoints`
- `eth_estimateUserOperationGas`
- `eth_sendUserOperation`
- `eth_getUserOperationByHash`
- `eth_getUserOperationReceipt`
- `pm_getPaymasterStubData`
- `pm_getPaymasterData`

```bash
curl --request POST "$DALP_URL/bundler/$CHAIN_ID" \
  --header "x-api-key: $DALP_API_KEY" \
  --header "content-type: application/json" \
  --data '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getUserOperationReceipt",
    "params": ["0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b"]
  }'
```

The answer is a JSON-RPC 2.0 envelope whose `result` holds the receipt from the receipt route, or `null`. The two read methods return the shapes this page shows for the REST reads. The receipt has no gas-used, gas-cost or log fields, and `eth_getUserOperationByHash` returns the stored operation, so a standard ERC-4337 client library that parses the full receipt or operation object does not parse these shapes. Map them in your client.

`eth_sendUserOperation` and `eth_estimateUserOperationGas` take the user operation and the EntryPoint address as their two parameters. The two paymaster methods take the user operation, the EntryPoint, the chain ID and a context value. Their chain ID must match the network in the path. They answer only when the organisation pays gas on that network through a paymaster, and otherwise refuse with `Paymaster sponsorship is not enabled`. The REST route `POST /api/v2/bundler/{chainId}/paymaster-data` returns the same paymaster data as `pm_getPaymasterData`. `pm_getPaymasterStubData` returns the same fields with a placeholder signature, for gas estimation only: an operation sent with it fails validation. Only `pm_getPaymasterData` and the REST route sign a ticket.

A batch holds 1 to 100 requests. A request or batch that holds only notifications answers HTTP 204 with no body. A `GET` on the endpoint answers HTTP 405.

## Refusals [#refusals]

The two interfaces report the same refusal in different detail. A REST route answers with the platform's error envelope: an HTTP status, a stable `code` and the catalogue message for that code. The JSON-RPC endpoint answers with a JSON-RPC error whose `message` carries the specific reason. The messages this page quotes are JSON-RPC messages. Over REST, branch on the HTTP status and `code`:

| Refusal | REST answer | JSON-RPC answer |
| --- | --- | --- |
| A missing field, a priority fee below the minimum, a `maxFeePerGas` below its floor, pre-verification gas below its price, the EntryPoint, the sender, a network the organisation cannot use, a replacement, an unsponsored paymaster call, or a simulation failure with no ERC-4337 code | HTTP 400 `TRANSACTION_QUEUE_INPUT_INVALID` | The specific reason in `error.message` |
| A simulation or prefund failure with an ERC-4337 code such as `AA21` | HTTP 422 `AA_VALIDATION_FAILED` | `error.message` is "Failed op" or "Failed op with revert", and `error.data.aaCode` holds the code |
| The organisation's sponsorship policy refuses the paymaster data, or the sponsorship ticket expired | HTTP 403 `PAYMASTER_SPONSORSHIP_DENIED` | `error.code` is `-32501` and `error.message` carries the reason |

The REST answer does not name the ERC-4337 code. To read it, send the same operation to `eth_sendUserOperation` on the JSON-RPC endpoint. If the operation passes this time, that call stores and bundles it.

Most of these refusals are caller errors: fix the field before you send again. Two can clear without a change to the request. A prefund refusal counts the payer's other operations still in flight, so it can pass once those settle or once you fund the payer. A network whose deployment is not active refuses until the deployment finishes. The REST routes answer that case with HTTP 400 `TRANSACTION_QUEUE_INPUT_INVALID`. The JSON-RPC endpoint answers it with HTTP 409 and a JSON-RPC error whose `data.retryable` is `false`: a retry does not help, so check the network's status in organisation settings and send again once it is active.

## Next steps [#next-steps]

- [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) explains how the platform bundles and settles operations, including one a bundle did not execute.
- [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) explains the validators, signer weights and threshold that authorise an operation.
- [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet) explains the bundler wallet that pays for each bundle.
- [The v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2) holds the full request and response schema for each route.
