> ## 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.

# Wallets

> Manage EOA and Safe smart wallets across EVM chains

## Overview

The API provides multi-chain wallet management for EVM-compatible blockchains. Each user can hold multiple wallets across different chains, with two distinct wallet types:

* **EOA (Externally Owned Account)** — user-controlled wallets added through verified ownership
* **Safe (Smart Wallet)** — smart contract wallets persisted under a user's primary EOA

Every user has a **primary EOA** wallet that serves as their main identity on-chain. The Safe paired with that EOA is provisioned through one of three cohort-specific flows configured at the SIS environment level — see [Onboarding cohorts](/dashboard/features#onboarding-cohort-summary). **The EOA signs. The Safe holds. The two are paired — always on the same chain.**

<Note>
  **Safe creation depends on your environment's cohort, but the surface is uniform:** `GET` and `POST /v0/user/me/onboarding/safe` serve every cohort — the response carries a `mode` discriminator and a cohort-specific `config` block. The `POST` body is a shape-only payload (no `mode` field — the cohort is derived server-side).

  * `mode=agent_create2` (default): Sumvin's signing service deploys the Safe automatically and attaches an agent signer. Clients call `POST` with an empty body `{}` and receive `204 No Content`; the workflow does the work.
  * `mode=user_signed_deploy`: the user signs the Safe deployment UserOp themselves. **Recommended** for partners who want user-controlled wallets without an agent signer.
  * `mode=byo`: the user submits the address of an existing on-chain Safe (Bring Your Own Safe).

  See [Submitting the Safe step](/guides/onboarding#submitting-the-safe-step) for the per-cohort POST shapes, idempotency support, and rate-limit details.
</Note>

## Wallet Ownership Verification

Before a wallet can be registered, the user must prove they control it. The API supports three methods depending on your authentication provider and wallet type.

### Method 1: Dynamic Credential

If your platform uses Dynamic Labs for authentication, wallet ownership is verified automatically through the user's verified credentials.

<Tip>
  Building a React UI? `useCreateWallet()` from our [TanStack Query patterns](/guides/tanstack-query-patterns#setup) wraps this call with the production `request()` wrapper and `frontend:wallets:create` caller metadata.
</Tip>

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/wallets/ \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "credential_id": "cred_abc123def456"
  }'
```

The API retrieves the verified wallet address and chain from Dynamic's API using the credential ID in the user's JWT. If the credential is valid and the wallet address has been cryptographically verified during sign-in, the wallet is created.

**Response:** `201 Created`

```json theme={null}
{
  "id": 42,
  "user_id": 7,
  "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
  "chain_id": 1329,
  "is_primary": false,
  "is_eoa": true,
  "nickname": null,
  "created_at": 1704067200000,
  "_links": {
    "self": { "href": "/v0/wallets/42" },
    "user": { "href": "/v0/user/me" },
    "set-primary": { "href": "/v0/wallets/42", "method": "PATCH" },
    "delete": { "href": "/v0/wallets/42", "method": "DELETE" }
  }
}
```

| Error         | When                                                              |
| ------------- | ----------------------------------------------------------------- |
| `WAL-403-003` | The credential ID is not found in the user's verified credentials |

### Method 2: SIWE (Sign In With Ethereum)

For platforms that don't use Dynamic Labs, or when you need standalone wallet verification, use the SIWE challenge/verify flow.

**Step 1: Request a challenge**

```bash theme={null}
curl "https://api.sumvin.com/v0/auth/siwe/challenge?address=0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78&chain_id=1329" \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

**Response:** `200 OK`

```json theme={null}
{
  "message": "app.sumvin.com wants you to sign in with your Ethereum account:\n0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78\n\nSign in with your Ethereum account to verify wallet ownership.\n\nURI: https://app.sumvin.com\nVersion: 1\nChain ID: 1329\nNonce: a1b2c3d4e5f67890\nIssued At: 2026-02-13T10:00:00Z",
  "nonce": "a1b2c3d4e5f67890",
  "_links": {
    "verify": { "href": "/v0/auth/siwe/verify", "method": "POST" }
  }
}
```

**Step 2: Sign the message with the wallet's private key and submit**

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/auth/siwe/verify \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "<the challenge message>",
    "signature": "0x<signed message>"
  }'
```

If the signature is valid, the wallet is created automatically.

**Response:** `201 Created`

The response body matches the wallet object from Method 1.

<Info>
  SIWE challenges expire after 10 minutes. If the challenge expires before verification, request a new one.
</Info>

### Method 3: Manual Safe Creation

For advanced use cases where you need to create a Safe wallet directly (e.g., for agent-controlled wallets), use the manual Safe creation flow.

**Step 1: Get Safe configuration for the target chain**

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

**Response:** `200 OK`

```json theme={null}
{
  "chain_id": 1329,
  "factory_address": "0x...",
  "singleton_address": "0x...",
  "fallback_handler": "0x...",
  "paymaster": {
    "address": "0x...",
    "type": "pimlico"
  },
  "_links": {
    "submit": { "href": "/v0/safe/rpc", "method": "POST" }
  }
}
```

**Step 2: Build and sign the Safe creation transaction, then submit via the RPC endpoint**

The submit endpoint is the same one used for every on-chain action a Safe takes — it accepts a signed ERC-4337 UserOperation, sponsors gas, and returns a hash you can poll. See [Submitting UserOperations](/guides/safe-user-operations) for the full lifecycle, including how to interpret the terminal `included`, `failed`, and `rejected` statuses.

```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 "Content-Type: application/json" \
  -d '{
    "chain_id": 1329,
    "signed_transaction": "0x<signed Safe creation tx>"
  }'
```

**Response:** `202 Accepted`

```json theme={null}
{
  "tx_hash": "0xabc123...",
  "status": "submitted",
  "_links": {
    "register": { "href": "/v0/wallets/", "method": "POST" }
  }
}
```

**Step 3: Register the wallet with the transaction hash for on-chain validation**

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/wallets/ \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "tx_hash": "0xabc123...",
    "chain_id": 1329
  }'
```

The API validates the transaction on-chain to confirm the Safe was created by the authenticated user, then registers it.

**Response:** `201 Created`

<Warning>
  Manual Safe creation is an advanced flow. Most integrations should use Method 1 (Dynamic credential) or Method 2 (SIWE). Only use this method when you need programmatic control over Safe deployment parameters.
</Warning>

## Setting a Primary Wallet

Promote a wallet to primary status. This may trigger asynchronous Safe creation if no Safe exists on that chain:

<Tip>
  Using TanStack Query? `useUpdateWallet()` shows the [optimistic mutations](/guides/tanstack-query-patterns#optimistic-mutations) pattern end-to-end — it updates the wallet list in `onMutate`, rolls back in `onError`, and invalidates `queryKeys.wallets.all` in `onSettled`.
</Tip>

```bash theme={null}
curl -X PATCH https://api.sumvin.com/v0/wallets/42 \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>" \
  -H "Content-Type: application/json" \
  -d '{"is_primary": true}'
```

If a Safe already exists on that chain, you get **200 OK**. If a new Safe needs to be deployed, you get **202 Accepted** with a `safe_creation_event_id`:

```json theme={null}
{
  "id": 42,
  "user_id": 7,
  "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
  "chain_id": 1329,
  "is_primary": true,
  "is_eoa": true,
  "safe_creation_event_id": "evt_abc123def456",
  "created_at": 1704067200000,
  "_links": {
    "self": { "href": "/v0/wallets/42" },
    "user": { "href": "/v0/user/me" },
    "safe-status": { "href": "/v0/user/me" }
  }
}
```

### Primary chain is locked once a Safe is deployed

Once a Safe smart wallet is persisted for a user (via the `byo_safe` step, the `safe_deploy` user-signed flow, or agent-driven Safe creation), the user's primary chain is locked. Promoting an EOA on a *different* chain to primary returns `400 Bad Request` with error code **`WAL-400-004`** (Primary Chain Locked):

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/wal-400-004",
  "title": "Primary Chain Locked",
  "status": 400,
  "detail": "Cannot change primary chain to 1: a Safe wallet is already persisted on chain 1329. Primary chain is locked once a Safe is deployed.",
  "instance": "/v0/wallets/42",
  "error_code": "WAL-400-004"
}
```

This guard prevents an in-place chain switch from orphaning the user's existing Safe. If you genuinely need a Safe on a different chain, that is a separate flow — contact your Sumvin point of contact, this isn't currently a self-serve operation.

### Polling for Safe Creation

When you receive a 202, poll the user profile to check Safe creation progress:

<Tip>
  In React, `useUser()` already serves the profile with [stale-while-revalidate](/guides/tanstack-query-patterns#stale-while-revalidate) — instead of a manual `setTimeout` loop, set a short `refetchInterval` until `safe_creation_status === 'completed'`.
</Tip>

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

The `safe_creation_status` field on the user tracks progress. Once `completed`, the `primary_smart_wallet_address` field contains the deployed Safe address.

## Wallet Types

| Type | `is_eoa` | Created By                                                         | Can Be Primary             | Can Be Deleted       |
| ---- | -------- | ------------------------------------------------------------------ | -------------------------- | -------------------- |
| EOA  | `true`   | Your application via ownership verification                        | Yes                        | Yes (if not primary) |
| Safe | `false`  | System (automatically when primary EOA is set) or Manual Safe flow | Yes (auto-set on creation) | No                   |

**EOA wallets** are standard Ethereum accounts controlled by a private key. Your application registers them after ownership verification so the API can track balances and transactions.

**Safe wallets** are smart contract wallets deployed by the system (or manually via Method 3). They provide multisig capabilities and are paired with the user's primary EOA on the same chain. System-created Safe wallets cannot be deleted — they are managed entirely by the system.

A third kind of record is stored alongside these but is not a wallet type you create. **Signer keys** are addresses recorded as signers on the account's Safe rather than wallets the account holder chose. A [CLI mandate key](/identity/agent-signers#agent-signers-and-other-safe-owners) — the key the Sumvin CLI holds locally so a developer can sign mandates as the user — is the one kind in use today. They are kept apart from the rest of the account's wallets:

* They are omitted from `GET /v0/wallets` and from `GET /v0/user/me?expand=wallets`, so those lists hold only wallets the account holder can transact from. Filters such as `is_eoa=true` do not bring them back.
* They remain readable at `GET /v0/wallets/{wallet_id}` by their own wallet ID.
* They cannot be deleted. `DELETE /v0/wallets/{wallet_id}` refuses any wallet carrying a signer role with `WAL-403-005`, because soft-deleting the record would not remove the key from the Safe on chain — the account would read as though the key were revoked while it could still sign.
* They do not count as wallets the account holder owns for authorization purposes. A contract deployed by a signer key is not evidence the account holder deployed it, so `POST /v0/wallets` with `method=txn_hash` returns `WAL-403-004` in that case.

## Multi-Chain Support

The API supports wallets across all major EVM-compatible chains:

| Chain           | Chain ID | Native Token |
| --------------- | -------- | ------------ |
| Sei             | `1329`   | SEI          |
| Ethereum        | `1`      | ETH          |
| Optimism        | `10`     | ETH          |
| Polygon         | `137`    | MATIC        |
| Arbitrum One    | `42161`  | ETH          |
| Avalanche       | `43114`  | AVAX         |
| BNB Smart Chain | `56`     | BNB          |

Each wallet is scoped to a single chain. A user can have wallets on multiple chains, but the primary EOA and its corresponding Safe must be on the same chain.

## Safe Creation Flow

When you set a primary EOA on a chain where no Safe exists, the system deploys one asynchronously.

```mermaid theme={null}
stateDiagram-v2
    [*] --> WalletVerified : Ownership verified
    WalletVerified --> SetPrimary : PATCH /v0/wallets/{id}
    SetPrimary --> SafeExists : Safe already on chain
    SetPrimary --> SafeCreation : No Safe on chain

    SafeExists --> Done200 : 200 OK
    SafeCreation --> Processing : 202 Accepted

    Processing --> Completed : Safe deployed
    Processing --> Failed : Deployment error

    Completed --> Done200 : Poll GET /v0/user/me

    note right of Processing : Poll user profile for status
```

### Safe Creation Statuses

| Status       | Meaning                                                        |
| ------------ | -------------------------------------------------------------- |
| `pending`    | Initial state before Safe creation is requested                |
| `processing` | Safe deployment is in progress on-chain                        |
| `completed`  | Safe deployed successfully — address available on user profile |
| `failed`     | Deployment failed — contact support                            |

<Warning>
  **Applies to the `ai_agent` cohort only.** Agent-deployed Safe creation requires an active agent signer on the target chain. If signer setup is still in progress, setting a primary wallet returns 424 Failed Dependency. Wait for onboarding to complete before setting a primary.

  Users in the `user_signed_deploy` or `byo_safe` cohorts have no agent signer and never see this 424. See [Onboarding cohorts](/dashboard/features#onboarding-cohort-summary).
</Warning>

## Endpoints

### List Wallets

```
GET /v0/wallets
```

Returns all wallets for the authenticated user. Supports filtering and expansion.

<Tip>
  Building a React UI? `useWallets(filters?)` from our [query patterns](/guides/tanstack-query-patterns#query-patterns) gives you a 5-minute `staleTime` and `isNonRetryable` retry gating. Pair it with `useWalletAssets(walletId)` for per-wallet balances and `useWalletBalances(walletId)` for the aggregated summary.
</Tip>

**Query parameters:**

| Parameter  | Type    | Description                                      |
| ---------- | ------- | ------------------------------------------------ |
| `chain_id` | integer | Filter by blockchain chain ID                    |
| `is_eoa`   | boolean | `true` for EOA wallets, `false` for Safe wallets |
| `expand`   | string  | Expand related resources. Options: `cards`       |

**Response:** 200 OK

```bash theme={null}
# All wallets on Sei
curl "https://api.sumvin.com/v0/wallets?chain_id=1329" \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"

# Only Safe wallets
curl "https://api.sumvin.com/v0/wallets?is_eoa=false" \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"

# Include linked payment cards
curl "https://api.sumvin.com/v0/wallets?expand=cards" \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
```

### Get Wallet

```
GET /v0/wallets/{wallet_id}
```

Returns full details for a specific wallet.

**Query parameters:**

| Parameter | Type   | Description                                |
| --------- | ------ | ------------------------------------------ |
| `expand`  | string | Expand related resources. Options: `cards` |

**Response:** 200 OK

### Create Wallet

The create wallet endpoint accepts different request bodies depending on the ownership verification method. See [Wallet Ownership Verification](#wallet-ownership-verification) above for the full flow for each method.

```
POST /v0/wallets
```

**Method 1 — Dynamic credential:**

| Field           | Type   | Required | Description                                                     |
| --------------- | ------ | -------- | --------------------------------------------------------------- |
| `credential_id` | string | Yes      | Dynamic Labs credential ID from the user's verified credentials |

**Method 2 — SIWE:**

Wallet creation happens automatically when the SIWE verify call succeeds. No separate wallet create call is needed.

* [`POST /v0/auth/siwe/verify`](/api-reference/siwe-auth/verify-and-create-wallet) — Verify signature and create wallet.
* [`POST /v0/wallets`](/api-reference/wallets/create-wallet) — Direct wallet create (not required for SIWE).

**Method 3 — Manual Safe (with tx hash):**

| Field      | Type    | Required | Description                                     |
| ---------- | ------- | -------- | ----------------------------------------------- |
| `tx_hash`  | string  | Yes      | Transaction hash of the on-chain Safe creation  |
| `chain_id` | integer | Yes      | Blockchain chain ID where the Safe was deployed |

**Response:** `201 Created`

Returns the created wallet with `_links` including `set-primary` and `delete` actions.

### Update Wallet

```
PATCH /v0/wallets/{wallet_id}
```

Updates wallet properties or promotes to primary status.

**Request body:**

| Field        | Type    | Description                             |
| ------------ | ------- | --------------------------------------- |
| `is_primary` | boolean | Set `true` to make this the primary EOA |
| `nickname`   | string  | Update the friendly label               |

**Response:** 200 OK when the update is synchronous, **202 Accepted** when Safe creation is triggered.

When the response is 202, it includes:

* `safe_creation_event_id` — event ID for tracking
* `_links.safe-status` — link to poll for Safe deployment completion

### Delete Wallet

```
DELETE /v0/wallets/{wallet_id}
```

Soft-deletes a wallet. The record is retained for audit purposes but no longer appears in lists.

<Tip>
  In React, `useDeleteWallet()` optimistically removes the wallet from the list and invalidates the entire `queryKeys.wallets.all` subtree on settle (success or rollback) — see the [invalidation strategy](/guides/tanstack-query-patterns#invalidation-strategy) page for the full key pyramid.
</Tip>

**Response:** 204 No Content

Primary wallets cannot be deleted. Set another wallet as primary first, then delete the old one.

### List Wallet Assets

```
GET /v0/wallets/{wallet_id}/assets
```

Returns all token balances held in a specific wallet, with USD valuations.

**Response:** 200 OK

```json theme={null}
{
  "wallet_id": 42,
  "assets": [
    {
      "asset_id": 1,
      "symbol": "ETH",
      "name": "Ethereum",
      "chain_id": 1329,
      "balance": "1.5",
      "balance_usd": "4875.00",
      "balance_updated_at": 1704067200000,
      "transactions_href": "/v0/wallets/42/assets/1/transactions"
    },
    {
      "asset_id": 5,
      "symbol": "USDC",
      "name": "USD Coin",
      "chain_id": 1329,
      "balance": "2500",
      "balance_usd": "2500.00",
      "balance_updated_at": 1704067200000,
      "transactions_href": "/v0/wallets/42/assets/5/transactions"
    }
  ],
  "total_assets": 2,
  "_links": {
    "self": { "href": "/v0/wallets/42/assets" },
    "wallet": { "href": "/v0/wallets/42" },
    "balances": { "href": "/v0/wallets/42/balances" },
    "transactions": { "href": "/v0/transactions?wallet_id=42" },
    "user": { "href": "/v0/user/me" }
  }
}
```

### List Asset Transactions

```
GET /v0/wallets/{wallet_id}/assets/{asset_id}/transactions
```

Returns paginated transactions for a specific asset in a wallet.

**Query parameters:**

| Parameter | Type    | Default | Description                                          |
| --------- | ------- | ------- | ---------------------------------------------------- |
| `offset`  | integer | `0`     | Pagination offset                                    |
| `limit`   | integer | `50`    | Results per page (max 100)                           |
| `from`    | integer | —       | Filter transactions after this timestamp (epoch ms)  |
| `to`      | integer | —       | Filter transactions before this timestamp (epoch ms) |

**Response:** 200 OK

```json theme={null}
{
  "wallet_id": 42,
  "asset_id": 1,
  "asset_symbol": "ETH",
  "transactions": [
    {
      "id": 101,
      "external_id": "0xabc123",
      "type": "transfer",
      "status": "confirmed",
      "direction": "incoming",
      "amount": "0.5",
      "created_at": 1704067200000,
      "tx_hash": "0x9f8e7d6c5b4a3210fedcba9876543210abcdef0123456789abcdef0123456789",
      "merchant_name": null
    }
  ],
  "total": 1,
  "offset": 0,
  "limit": 50,
  "_links": {
    "self": { "href": "/v0/wallets/42/assets/1/transactions?offset=0&limit=50" },
    "wallet": { "href": "/v0/wallets/42" },
    "asset": { "href": "/v0/assets/1" },
    "wallet_assets": { "href": "/v0/wallets/42/assets" },
    "user": { "href": "/v0/user/me" }
  }
}
```

Pagination links (`next`, `prev`) are included in `_links` when applicable.

### Get Balance Summary

```
GET /v0/wallets/{wallet_id}/balances
```

Returns an aggregated balance summary for a wallet, including all assets with USD valuations.

**Response:** 200 OK

```json theme={null}
{
  "summary": {
    "wallet_id": 42,
    "total_assets": 2,
    "last_updated": 1704067200000,
    "assets": [
      {
        "asset_id": 1,
        "symbol": "ETH",
        "name": "Ethereum",
        "chain_id": 1329,
        "balance": "1.5",
        "balance_usd": "4875.00",
        "balance_updated_at": 1704067200000,
        "transactions_href": "/v0/wallets/42/assets/1/transactions"
      }
    ]
  },
  "_links": {
    "self": { "href": "/v0/wallets/42/balances" },
    "wallet": { "href": "/v0/wallets/42" },
    "assets": { "href": "/v0/wallets/42/assets" },
    "transactions": { "href": "/v0/transactions?wallet_id=42" },
    "user": { "href": "/v0/user/me" }
  }
}
```

## Integration Patterns

### Choosing a Verification Method

| Method                | Best For                                    | Auth Provider |
| --------------------- | ------------------------------------------- | ------------- |
| Dynamic credential    | Platforms using Dynamic Labs                | Dynamic Labs  |
| SIWE challenge/verify | Any platform, standalone verification       | Any           |
| Manual Safe creation  | Agent wallets, programmatic Safe deployment | Any           |

### Handling Async Safe Creation

When the wallet update returns 202, implement a polling loop:

[`PATCH /v0/wallets/{wallet_id}`](/api-reference/wallets/update-wallet) — Update wallet (may return 202 when Safe creation is triggered).

```typescript theme={null}
async function waitForSafe(jwt: string, orgId: string): Promise<string> {
  while (true) {
    const res = await fetch("https://api.sumvin.com/v0/user/me", {
      headers: { "x-juno-jwt": jwt, "x-juno-orgid": orgId },
    });
    const user = await res.json();

    if (user.safe_creation_status === "completed") {
      return user.primary_smart_wallet_address;
    }
    if (user.safe_creation_status === "failed") {
      throw new Error("Safe deployment failed");
    }

    await new Promise((r) => setTimeout(r, 3000));
  }
}
```

```python theme={null}
import time
import requests

def wait_for_safe(jwt: str, org_id: str) -> str:
    while True:
        res = requests.get(
            "https://api.sumvin.com/v0/user/me",
            headers={"x-juno-jwt": jwt, "x-juno-orgid": org_id},
        )
        user = res.json()

        if user["safe_creation_status"] == "completed":
            return user["primary_smart_wallet_address"]
        if user["safe_creation_status"] == "failed":
            raise Exception("Safe deployment failed")

        time.sleep(3)
```

The typical Safe deployment takes 10–30 seconds depending on chain congestion.

### Building a Wallet Selector

Fetch all wallets and group by type:

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

```typescript theme={null}
const res = await fetch("https://api.sumvin.com/v0/wallets", {
  headers: { "x-juno-jwt": jwt, "x-juno-orgid": orgId },
});
const { wallets } = await res.json();

const eoaWallets = wallets.filter((w) => w.is_eoa);
const safeWallets = wallets.filter((w) => !w.is_eoa);
const primaryEoa = eoaWallets.find((w) => w.is_primary);
```

### Following HAL Links

Responses include `_links` for discoverable navigation. Non-primary wallets include `set-primary` and `delete` action links. Primary wallets omit these since the actions are not permitted.

```typescript theme={null}
const wallet = await fetch(`https://api.sumvin.com/v0/wallets/42`, {
  headers: { "x-juno-jwt": jwt, "x-juno-orgid": orgId },
}).then((r) => r.json());

if (wallet._links["set-primary"]) {
  // Wallet can be promoted
  await fetch(`https://api.sumvin.com${wallet._links["set-primary"].href}`, {
    method: "PATCH",
    headers: {
      "x-juno-jwt": jwt,
      "x-juno-orgid": orgId,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ is_primary: true }),
  });
}
```

## Error Handling

All error responses follow RFC 7807 Problem Details. See [Error Handling](/error-handling) for the full reference.

### Wallet Error Codes

| Code          | Status | Description                                                         | Recovery                                                                                                        |
| ------------- | ------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `WAL-400-001` | 400    | Invalid Ethereum address format                                     | Provide a valid 0x-prefixed, checksummed address                                                                |
| `WAL-400-002` | 400    | Chain ID does not match primary chain                               | Use the same chain as the primary wallet                                                                        |
| `WAL-403-001` | 403    | Cannot delete primary wallet                                        | Set another wallet as primary first                                                                             |
| `WAL-403-002` | 403    | Wallet belongs to another user                                      | Verify the wallet ID                                                                                            |
| `WAL-403-003` | 403    | Wallet address not in verified credentials                          | Verify wallet ownership — see [Wallet Ownership Verification](#wallet-ownership-verification)                   |
| `WAL-403-004` | 403    | The transaction's deployer is not a wallet owned by this user       | Deploy from an address already registered to the user, or register that address first. Signer keys do not count |
| `WAL-403-005` | 403    | Cannot delete a wallet registered as a signer on the account's Safe | Remove it as a signer first; deleting the record alone would not remove it from the Safe                        |
| `WAL-404-001` | 404    | Wallet not found                                                    | Check the wallet ID; list wallets via the wallets endpoint (see below)                                          |
| `WAL-409-001` | 409    | Wallet already exists for this address and chain                    | Use the existing wallet                                                                                         |
| `USR-424-001` | 424    | No active agent signer (precondition failed)                        | Wait for signer setup to complete                                                                               |

[`GET /v0/wallets`](/api-reference/wallets/list-wallets) — List wallets.

### Asset Error Codes

| Code          | Status | Description               | Recovery                            |
| ------------- | ------ | ------------------------- | ----------------------------------- |
| `AST-404-001` | 404    | Asset not found in wallet | Verify the asset ID for this wallet |

## Reference Tables

### Wallet Response Fields

| Field                    | Type            | Description                                                   |
| ------------------------ | --------------- | ------------------------------------------------------------- |
| `id`                     | integer         | Internal wallet ID                                            |
| `user_id`                | integer         | Owning user's ID                                              |
| `address`                | string          | Wallet address (EOA or Safe contract)                         |
| `chain_id`               | integer         | Blockchain chain ID                                           |
| `is_primary`             | boolean         | Whether this is the user's primary wallet of its type         |
| `is_eoa`                 | boolean         | `true` for EOA, `false` for Safe                              |
| `nickname`               | string or null  | User-defined label                                            |
| `logo_uri`               | string or null  | Chain logo image URI                                          |
| `created_at`             | integer         | Creation timestamp (epoch milliseconds)                       |
| `deleted_at`             | integer or null | Soft-deletion timestamp if removed                            |
| `safe_creation_event_id` | string or null  | Present only in 202 responses when Safe creation is triggered |
| `cards`                  | array or null   | Linked payment cards (only with `expand=cards`)               |

### Wallet Asset Fields

| Field                | Type            | Description                                         |
| -------------------- | --------------- | --------------------------------------------------- |
| `asset_id`           | integer         | Asset identifier                                    |
| `symbol`             | string          | Token symbol (e.g. `ETH`, `USDC`)                   |
| `name`               | string          | Full token name                                     |
| `chain_id`           | integer or null | Chain where the asset exists                        |
| `balance`            | string          | Token balance as a decimal string                   |
| `balance_usd`        | string or null  | USD valuation (null if price unavailable)           |
| `balance_updated_at` | integer         | Last balance refresh timestamp (epoch milliseconds) |

### Asset Transaction Fields

| Field           | Type           | Description                                      |
| --------------- | -------------- | ------------------------------------------------ |
| `id`            | integer        | Transaction ID                                   |
| `external_id`   | string or null | External reference ID                            |
| `type`          | string         | Transaction type (e.g. `transfer`)               |
| `status`        | string         | Transaction status (e.g. `confirmed`, `pending`) |
| `direction`     | string         | `incoming` or `outgoing`                         |
| `amount`        | string         | Transaction amount as a decimal string           |
| `created_at`    | integer        | Transaction timestamp (epoch milliseconds)       |
| `tx_hash`       | string or null | On-chain transaction hash                        |
| `merchant_name` | string or null | Merchant name for card-funded transactions       |

## Next Steps

Wallet management sits downstream of the onboarding state machine — run [onboarding](/guides/onboarding) to completion before standing up wallets, and confirm your [authentication](/authentication) against the Platform API JWT model. **The wallet is not the identity. The Safe on it is — and the Safe is what every agent, card, and credential anchors to.**
