Skip to main content

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) — multisig smart contract wallets created automatically by the system
Every user has a primary EOA wallet that serves as their main identity on-chain. When a primary EOA is set, the system automatically deploys a Safe smart wallet on the same chain. This Safe creation is asynchronous — the API returns 202 Accepted and you poll for completion. The EOA signs. The Safe holds. The two are paired — always on the same chain.

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.
Building a React UI? useCreateWallet() from our TanStack Query patterns wraps this call with the production request() wrapper and frontend:wallets:create caller metadata.
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
{
  "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" }
  }
}
ErrorWhen
WAL-403-003The 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
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
{
  "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
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.
SIWE challenges expire after 10 minutes. If the challenge expires before verification, request a new one.

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
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
{
  "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
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
{
  "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
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
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.

Setting a Primary Wallet

Promote a wallet to primary status. This may trigger asynchronous Safe creation if no Safe exists on that chain:
Using TanStack Query? useUpdateWallet() shows the optimistic mutations pattern end-to-end — it updates the wallet list in onMutate, rolls back in onError, and invalidates queryKeys.wallets.all in onSettled.
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:
{
  "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" }
  }
}

Polling for Safe Creation

When you receive a 202, poll the user profile to check Safe creation progress:
In React, useUser() already serves the profile with stale-while-revalidate — instead of a manual setTimeout loop, set a short refetchInterval until safe_creation_status === 'completed'.
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

Typeis_eoaCreated ByCan Be PrimaryCan Be Deleted
EOAtrueYour application via ownership verificationYesYes (if not primary)
SafefalseSystem (automatically when primary EOA is set) or Manual Safe flowYes (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.

Multi-Chain Support

The API supports wallets across all major EVM-compatible chains:
ChainChain IDNative Token
Sei1329SEI
Ethereum1ETH
Optimism10ETH
Polygon137MATIC
Arbitrum One42161ETH
Avalanche43114AVAX
BNB Smart Chain56BNB
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.

Safe Creation Statuses

StatusMeaning
pendingInitial state before Safe creation is requested
processingSafe deployment is in progress on-chain
completedSafe deployed successfully — address available on user profile
failedDeployment failed — contact support
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.

Endpoints

List Wallets

GET /v0/wallets
Returns all wallets for the authenticated user. Supports filtering and expansion.
Building a React UI? useWallets(filters?) from our 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.
Query parameters:
ParameterTypeDescription
chain_idintegerFilter by blockchain chain ID
is_eoabooleantrue for EOA wallets, false for Safe wallets
expandstringExpand related resources. Options: cards
Response: 200 OK
# 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:
ParameterTypeDescription
expandstringExpand 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 above for the full flow for each method.
POST /v0/wallets
Method 1 — Dynamic credential:
FieldTypeRequiredDescription
credential_idstringYesDynamic 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. Method 3 — Manual Safe (with tx hash):
FieldTypeRequiredDescription
tx_hashstringYesTransaction hash of the on-chain Safe creation
chain_idintegerYesBlockchain 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:
FieldTypeDescription
is_primarybooleanSet true to make this the primary EOA
nicknamestringUpdate 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.
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 page for the full key pyramid.
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
{
  "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:
ParameterTypeDefaultDescription
offsetinteger0Pagination offset
limitinteger50Results per page (max 100)
fromintegerFilter transactions after this timestamp (epoch ms)
tointegerFilter transactions before this timestamp (epoch ms)
Response: 200 OK
{
  "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
{
  "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

MethodBest ForAuth Provider
Dynamic credentialPlatforms using Dynamic LabsDynamic Labs
SIWE challenge/verifyAny platform, standalone verificationAny
Manual Safe creationAgent wallets, programmatic Safe deploymentAny

Handling Async Safe Creation

When the wallet update returns 202, implement a polling loop: PATCH /v0/wallets/{wallet_id} — Update wallet (may return 202 when Safe creation is triggered).
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));
  }
}
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:
curl https://api.sumvin.com/v0/wallets \
  -H "x-juno-jwt: <token>" \
  -H "x-juno-orgid: <your-org-id>"
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);
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.
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 for the full reference.

Wallet Error Codes

CodeStatusDescriptionRecovery
WAL-400-001400Invalid Ethereum address formatProvide a valid 0x-prefixed, checksummed address
WAL-400-002400Chain ID does not match primary chainUse the same chain as the primary wallet
WAL-403-001403Cannot delete primary walletSet another wallet as primary first
WAL-403-002403Wallet belongs to another userVerify the wallet ID
WAL-403-003403Wallet address not in verified credentialsVerify wallet ownership — see Wallet Ownership Verification
WAL-404-001404Wallet not foundCheck the wallet ID; list wallets via the wallets endpoint (see below)
WAL-409-001409Wallet already exists for this address and chainUse the existing wallet
USR-424-001424No active agent signer (precondition failed)Wait for signer setup to complete
GET /v0/wallets — List wallets.

Asset Error Codes

CodeStatusDescriptionRecovery
AST-404-001404Asset not found in walletVerify the asset ID for this wallet

Reference Tables

Wallet Response Fields

FieldTypeDescription
idintegerInternal wallet ID
user_idintegerOwning user’s ID
addressstringWallet address (EOA or Safe contract)
chain_idintegerBlockchain chain ID
is_primarybooleanWhether this is the user’s primary wallet of its type
is_eoabooleantrue for EOA, false for Safe
nicknamestring or nullUser-defined label
logo_uristring or nullChain logo image URI
created_atintegerCreation timestamp (epoch milliseconds)
deleted_atinteger or nullSoft-deletion timestamp if removed
safe_creation_event_idstring or nullPresent only in 202 responses when Safe creation is triggered
cardsarray or nullLinked payment cards (only with expand=cards)

Wallet Asset Fields

FieldTypeDescription
asset_idintegerAsset identifier
symbolstringToken symbol (e.g. ETH, USDC)
namestringFull token name
chain_idinteger or nullChain where the asset exists
balancestringToken balance as a decimal string
balance_usdstring or nullUSD valuation (null if price unavailable)
balance_updated_atintegerLast balance refresh timestamp (epoch milliseconds)

Asset Transaction Fields

FieldTypeDescription
idintegerTransaction ID
external_idstring or nullExternal reference ID
typestringTransaction type (e.g. transfer)
statusstringTransaction status (e.g. confirmed, pending)
directionstringincoming or outgoing
amountstringTransaction amount as a decimal string
created_atintegerTransaction timestamp (epoch milliseconds)
tx_hashstring or nullOn-chain transaction hash
merchant_namestring or nullMerchant name for card-funded transactions

Next Steps

Wallet management sits downstream of the onboarding state machine — run onboarding to completion before standing up wallets, and confirm your 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.