> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sumvin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Submitting UserOperations

> End-to-end walkthrough: discover the Safe, build a UserOperation, submit for sponsorship, and poll for the on-chain outcome.

# Submitting UserOperations

This guide walks through the complete flow for sending an action through a user's Safe smart account. By the end you will have submitted a sponsored UserOperation, polled it to a terminal status, and read the underlying transaction receipt.

For background on what a UserOperation is and how the lifecycle states relate, read [Sponsored UserOperations](/concepts/sponsored-user-operations) first.

## Prerequisites

* An authenticated user. See the [onboarding guide](/guides/onboarding) for the user creation flow.
* An active signer for the user on the target chain. Signers are provisioned during onboarding; see [Safes and identity](/concepts/safes-and-identity) for how the signer relates to the Safe.

## Step 1: Discover the Safe

Before constructing a UserOperation you need the Safe address (which becomes the `sender`) and, if the Safe is not yet deployed on this chain, the factory parameters that will deploy it in the same bundle.

```bash theme={null}
curl "https://api.sumvin.com/v0/safe/config?chain_id=10" \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

**Response: `200 OK`**

```json theme={null}
{
  "factory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
  "factory_data": "0x1688f0b9...",
  "predicted_safe_address": "0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
  "entrypoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
  "chain_id": 10,
  "signer_contract_address": "0xA1b2C3d4E5f6789012345678901234567890aBcD",
  "_links": {
    "self": { "href": "/v0/safe/config?chain_id=10" },
    "rpc": { "href": "/v0/safe/rpc", "method": "POST" }
  }
}
```

The `predicted_safe_address` is deterministic from CREATE2. It is the address the Safe will deploy to, and it is **usable as a UserOperation `sender` even before deployment** — the first UserOperation can include `factory` and `factoryData` to deploy the Safe in the same bundle.

## Step 2: Construct the UserOperation

Build the UserOperation client-side. Most ERC-4337 SDKs (e.g., [`permissionless`](https://docs.pimlico.io/permissionless), [viem's account-abstraction module](https://viem.sh/account-abstraction)) handle nonce, gas estimation, and signature production. The full ERC-4337 specification is out of scope for this guide; refer to the [EIP](https://eips.ethereum.org/EIPS/eip-4337) for field-by-field semantics.

A minimal example:

```ts theme={null}
import { createPublicClient, http } from "viem";
import { optimism } from "viem/chains";
import { createBundlerClient } from "viem/account-abstraction";

const userOperation = {
  sender: predictedSafeAddress,
  nonce: 0n,
  factory: factoryAddress,         // omit on subsequent ops once Safe is deployed
  factoryData: factoryData,        // omit on subsequent ops once Safe is deployed
  callData: encodeExecuteCalldata(target, value, data),
  callGasLimit: 200_000n,
  verificationGasLimit: 350_000n,
  preVerificationGas: 50_000n,
  maxFeePerGas: 1_500_000_000n,
  maxPriorityFeePerGas: 1_000_000_000n,
  signature: await signWithUserSigner(...),
};
```

Sumvin populates the paymaster fields on your behalf — do not set `paymaster`, `paymasterData`, `paymasterVerificationGasLimit`, or `paymasterPostOpGasLimit` on the request body.

## Step 3: Submit for sponsorship

Send the signed UserOperation to `/v0/safe/rpc`. Include an `Idempotency-Key` header so retries are safe.

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/safe/rpc \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Idempotency-Key: 7b3d4e5f-1a2b-4c3d-9e8f-1234567890ab" \
  -H "Content-Type: application/json" \
  -d '{
    "chain_id": 10,
    "user_operation": {
      "sender": "0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
      "nonce": "0x0",
      "factory": "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67",
      "factoryData": "0x1688f0b9...",
      "callData": "0x6a7612020000...",
      "callGasLimit": "0x30d40",
      "verificationGasLimit": "0x55730",
      "preVerificationGas": "0xc350",
      "maxFeePerGas": "0x59682f00",
      "maxPriorityFeePerGas": "0x3b9aca00",
      "signature": "0xabc..."
    },
    "entrypoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
  }'
```

<Tip>
  The `Idempotency-Key` header should be a UUID v4 generated at request time. **Agent-originated UserOperations should always include one** — LLM retries on a transient network error must not result in double-sponsorship.
</Tip>

**Response: `202 Accepted`**

```json theme={null}
{
  "user_op_hash": "0x9f1a8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b",
  "status": "submitted",
  "_links": {
    "self": { "href": "/v0/safe/rpc", "method": "POST" },
    "status": { "href": "/v0/safe/rpc/0x9f1a8c.../status" }
  }
}
```

The 202 indicates that Sumvin has accepted the UserOperation and forwarded it to the bundler. It is **not** a guarantee that the operation will land on-chain.

### Idempotency replay

Re-sending the same request body with the same `Idempotency-Key` returns the original response. The replayed response includes an `X-Idempotent-Replay: true` response header so your client can distinguish replays from fresh submissions.

### Idempotency conflict

Re-using the same `Idempotency-Key` with a **different** request body returns `409 Conflict`:

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/saf-409-001",
  "title": "Idempotency Conflict",
  "status": 409,
  "detail": "Idempotency-Key already used with a different request body",
  "instance": "/v0/safe/rpc",
  "error_code": "SAF-409-001"
}
```

## Step 4: Poll for status

Use the `_links.status.href` from the previous response, or construct the URL directly: `GET /v0/safe/rpc/{user_op_hash}/status`.

```bash theme={null}
curl https://api.sumvin.com/v0/safe/rpc/0x9f1a8c.../status \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

While the operation is still in flight you will see one of `not_submitted`, `queued`, or `submitted`:

```json theme={null}
{
  "user_op_hash": "0x9f1a8c...",
  "status": "queued",
  "txn_hash": null,
  "block_number": null,
  "gas_used": null,
  "actual_gas_cost_wei": null,
  "success": null,
  "_links": {
    "self": { "href": "/v0/safe/rpc/0x9f1a8c.../status" },
    "rpc": { "href": "/v0/safe/rpc", "method": "POST" }
  }
}
```

### Terminal: `included`

When the UserOperation succeeds on-chain, the response is fully populated and includes a `receipt` link:

```json theme={null}
{
  "user_op_hash": "0x9f1a8c...",
  "status": "included",
  "txn_hash": "0xc0ffee...",
  "block_number": 121845760,
  "gas_used": 187432,
  "actual_gas_cost_wei": 281148000000000,
  "success": true,
  "_links": {
    "self": { "href": "/v0/safe/rpc/0x9f1a8c.../status" },
    "rpc": { "href": "/v0/safe/rpc", "method": "POST" },
    "receipt": { "href": "/v0/transactions/receipts/0xc0ffee..." }
  }
}
```

### Terminal: `failed`

The bundle landed but this UserOperation reverted on-chain. Same shape as `included`, but `success: false`. The transaction hash and `receipt` link are still populated because gas was burned:

```json theme={null}
{
  "user_op_hash": "0x9f1a8c...",
  "status": "failed",
  "txn_hash": "0xc0ffee...",
  "block_number": 121845760,
  "gas_used": 95212,
  "actual_gas_cost_wei": 142818000000000,
  "success": false,
  "_links": {
    "self": { "href": "/v0/safe/rpc/0x9f1a8c.../status" },
    "rpc": { "href": "/v0/safe/rpc", "method": "POST" },
    "receipt": { "href": "/v0/transactions/receipts/0xc0ffee..." }
  }
}
```

### Terminal: `rejected`

The bundler declined the UserOperation pre-submission. **No transaction hash, no receipt link, no gas burned:**

```json theme={null}
{
  "user_op_hash": "0x9f1a8c...",
  "status": "rejected",
  "txn_hash": null,
  "block_number": null,
  "gas_used": null,
  "actual_gas_cost_wei": null,
  "success": null,
  "_links": {
    "self": { "href": "/v0/safe/rpc/0x9f1a8c.../status" },
    "rpc": { "href": "/v0/safe/rpc", "method": "POST" }
  }
}
```

See [Sponsored UserOperations](/concepts/sponsored-user-operations#rejected-vs-failed-failure-mode-taxonomy) for the full distinction between `rejected` and `failed`.

## Step 5: Follow the receipt link

When `_links.receipt` is present, `GET` it to retrieve the full on-chain transaction record — logs, internal calls, and the canonical receipt object — through Sumvin's transaction history API.

```bash theme={null}
curl https://api.sumvin.com/v0/transactions/receipts/0xc0ffee... \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

### Degraded finalisation: `503 Service Unavailable`

For user-signed Safe-deployment UserOperations (the `safe_deploy` onboarding step under `user_signed_deploy`), the status-poll endpoint runs an extra verification step after the bundler reports `included`: it verifies the deployed contract is a Safe with the user EOA as sole owner, persists the wallet, and advances onboarding.

Three distinct outcomes:

* **200 OK with `included`** — finalisation succeeded; the wallet is persisted and onboarding has advanced.
* **400 with `SAF-400-012` (Safe Deploy Verification Failed)** — the deployed contract is not a Safe (e.g. wrong factory) or has the wrong owner set. **Permanent failure** — re-polling will not fix it.
* **503 with `SAF-503-001` (Safe Deploy Finalisation Degraded)** — finalisation hit a transient downstream failure (e.g. the verifier RPC is briefly unavailable). The UserOperation is still recorded as included on-chain. The response includes a `Retry-After` header (default `5` seconds) — clients should pause polling for that duration and retry.

Treat `503` + `Retry-After` as a polling pause, not a hard failure. The next status poll will re-run finalisation against the same UserOperation hash.

## Error reference

| Code          | Status                  | When                                                                                                                                                              |
| ------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SAF-409-001` | 409 Conflict            | `Idempotency-Key` reused with a different request body.                                                                                                           |
| `SAF-400-012` | 400 Bad Request         | User-signed Safe deploy: the deployed contract failed verification (not a Safe, wrong owners, or wrong contract version). Permanent — do not retry.               |
| `SAF-502-004` | 502 Bad Gateway         | The bundler returned a status value Sumvin does not recognise. Treat as transient and retry.                                                                      |
| `SAF-502-005` | 502 Bad Gateway         | Failed to fetch the on-chain receipt for an in-flight UserOperation. Retry the poll.                                                                              |
| `SAF-503-001` | 503 Service Unavailable | User-signed Safe deploy finalisation hit a transient error after the UserOperation was included on-chain. Retry the status poll after the `Retry-After` interval. |

## See also

* [Sponsored UserOperations concept](/concepts/sponsored-user-operations) — status vocabulary and lifecycle reference.
* [Wallets guide](/guides/wallets) — Safe creation and wallet ownership flows that precede this one.
* [HAL, RFC 7807, idempotency, and pagination](/concepts/hal-rfc7807-idempotency-pagination) — the conventions for navigation links and error envelopes.
