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" }
}
}
| 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
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
| 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.
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.
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 |
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
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:
| 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
# 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 above for the full flow for each method.
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.
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.
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:
| 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
{
"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
| 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} — 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);
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.
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
| 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 |
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 — 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 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.