Overview
The JIL Sovereign Wallet API provides programmatic access to non-custodial MPC wallet infrastructure.
Users hold their own keys via MPC 2-of-3 threshold signing with WebAuthn passkeys. All transactions
are signed client-side and verified on the JIL L1 ledger with 14-of-20 validator consensus.
Base URLs
| Environment | Base URL |
| Production | https://wallet.jilsovereign.com |
| Devnet | https://devnet-wallet.jilsovereign.com |
| Local (Docker) | http://wallet-api:8002 |
Protocol
| Property | Value |
| Transport | HTTPS (TLS 1.3 required in production) |
| Content Type | application/json |
| Character Encoding | UTF-8 |
| Date Format | ISO 8601 (2026-02-09T14:30:00.000Z) |
| Amount Precision | String-encoded decimals (e.g. "1500.00") |
| Auth Model | JWT Bearer token + WebAuthn passkey signing |
Authentication
The Wallet API uses a two-layer authentication model. All requests require a JWT Bearer token.
Transaction signing additionally requires a WebAuthn passkey challenge-response.
Bearer Token
Include the JWT token in the Authorization header with every request.
GET /wallet/balances HTTP/1.1
Host: wallet.jilsovereign.com
Authorization: Bearer eyJhbGciOiJFZERTQSIs...
Content-Type: application/json
Two-Phase Transaction Signing
Sending funds uses a two-phase flow: (1) request a WebAuthn challenge via /send/options,
(2) sign with the user's passkey and submit via /send/submit. This ensures the user's
private key never leaves their device.
Token Expiry: JWT tokens expire after 7 days. Refresh tokens are not supported.
The user must re-authenticate to obtain a new token.
Passkey Management
POST /auth/passkey/register/options
Get WebAuthn registration options for creating a new passkey. Returns the challenge and public key creation parameters for the authenticator.
Response (200 OK)
{
"publicKey": {
"challenge": "base64url-encoded-challenge",
"rp": { "name": "JIL Sovereign", "id": "jilsovereign.com" },
"user": {
"id": "base64url-user-id",
"name": "user@example.com",
"displayName": "Alice"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"authenticatorSelection": {
"userVerification": "required",
"residentKey": "preferred"
},
"timeout": 60000
}
}
POST /auth/passkey/register/verify
Verify and complete passkey registration with the attestation response from the authenticator.
Request Body
| Field | Type | Required | Description |
id | string | required | Credential ID from the authenticator |
rawId | string | required | Raw credential ID (base64url-encoded) |
response.clientDataJSON | string | required | Client data JSON from authenticator (base64url) |
response.attestationObject | string | required | Attestation object from authenticator (base64url) |
Response (200 OK)
{
"verified": true,
"credentialId": "cred_a1b2c3d4e5f6",
"publicKey": "base64url-encoded-public-key"
}
POST /auth/passkey/authenticate/options
Get WebAuthn authentication options for signing in with an existing passkey.
Response (200 OK)
{
"publicKey": {
"challenge": "base64url-encoded-challenge",
"rpId": "jilsovereign.com",
"allowCredentials": [
{ "type": "public-key", "id": "base64url-cred-id" }
],
"userVerification": "required",
"timeout": 60000
}
}
POST /auth/passkey/authenticate/verify
Verify the passkey assertion and issue a JWT Bearer token.
Response (200 OK)
{
"token": "eyJhbGciOiJFZERTQSIs...",
"expiresAt": "2026-02-16T14:30:00.000Z",
"accountId": "acc_abc123"
}
Wallet Operations
GET /wallet/balances
Get all asset balances for the authenticated user's wallet. Returns each asset with its balance and USD value.
Response (200 OK)
{
"accountId": "acc_abc123",
"zone": "protected",
"assets": [
{
"symbol": "JIL",
"balance": "1000.00",
"usdValue": "2500.00"
},
{
"symbol": "USDC",
"balance": "500.00",
"usdValue": "500.00"
},
{
"symbol": "jBTC",
"balance": "0.25000000",
"usdValue": "24500.00"
}
],
"protectionTier": "premium",
"protectionCoverage": "250000.00"
}
POST /wallet/send/options
Get transaction intent and WebAuthn challenge for signing a payment. This is the first step of the
two-phase send flow. The challenge must be signed by the user's passkey before submitting.
Request Body
| Field | Type | Required | Description |
to | string | required | Recipient address or @handle (e.g., "@alice.jil") |
asset | string | required | Asset symbol (e.g. JIL, USDC, jBTC) |
amount | string | required | Amount to send as decimal string (e.g. "100.00") |
memo | string | optional | Optional memo (max 256 characters) |
Response (200 OK)
{
"txIntent": {
"from": "acc_abc123",
"to": "jil1xyz...",
"asset": "JIL",
"amount": "100.00",
"nonce": 42
},
"challenge": {
"publicKey": {
"challenge": "base64url-challenge",
"rpId": "jilsovereign.com",
"userVerification": "required"
}
},
"estimatedFee": "0.01"
}
POST /wallet/send/submit
Submit a signed payment transaction. Requires the transaction intent from /send/options and the signed WebAuthn credential from the user's passkey.
Request Body
| Field | Type | Required | Description |
txIntent | object | required | Transaction intent object from /send/options |
credential | object | required | WebAuthn assertion response from passkey signing |
Response (200 OK)
{
"txId": "tx_7f3a9b2c4e1d",
"status": "confirmed",
"receipt": {
"blockHeight": 1000042,
"blockHash": "0x4e7f1a...c3d8b2",
"timestamp": "2026-02-09T14:30:02.345Z",
"gasUsed": 21000
},
"from": "acc_abc123",
"to": "jil1xyz...",
"asset": "JIL",
"amount": "100.00"
}
GET /wallet/transactions
Get transaction history for the authenticated wallet. Supports cursor-based pagination and filtering.
Query Parameters
| Parameter | Type | Required | Description |
cursor | string | optional | Cursor for pagination. Omit for first page. |
limit | integer | optional | Results per page, 1-100. Default: 25. |
asset | string | optional | Filter by asset symbol. |
Response (200 OK)
{
"transactions": [
{
"txId": "tx_7f3a9b2c4e1d",
"type": "send",
"asset": "JIL",
"amount": "-100.00",
"to": "@alice.jil",
"status": "confirmed",
"timestamp": "2026-02-09T14:30:02.345Z"
}
],
"pagination": {
"cursor": "eyJpZCI6InR4XzdmM2E5YjJjNGUxZCJ9",
"has_more": true
}
}
GET /wallet/wrapped-assets
List available wrapper tokens (jBTC, jETH, jUSDC) with deposit and unwrap information.
Response (200 OK)
{
"assets": [
{
"id": "jbtc-eth-wbtc",
"symbol": "jBTC",
"originalChain": "eth",
"originalToken": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"decimals": 8
},
{
"id": "jeth-eth-native",
"symbol": "jETH",
"originalChain": "eth",
"originalToken": "0x0000000000000000000000000000000000000000",
"decimals": 18
},
{
"id": "jusdc-eth-usdc",
"symbol": "jUSDC",
"originalChain": "eth",
"originalToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6
}
]
}
Wrapper Tokens: jBTC, jETH, and jUSDC are 1:1 backed wrapper tokens. Deposits on the
external chain are confirmed and auto-minted on the JIL L1. Unwraps burn the wrapper and release
the original asset on the external chain.
Handle Resolution
GET /handle/resolve/{handle}
Resolve a human-readable @handle to its on-chain address. No authentication required.
Path Parameters
| Parameter | Type | Required | Description |
handle | string | required | Handle without @ prefix (e.g., "alice.jil") |
Response (200 OK)
{
"handle": "alice.jil",
"address": "jil1abc123def456...",
"profile": {
"displayName": "Alice",
"avatar": "https://cdn.jilsovereign.com/avatars/alice.jpg"
}
}
POST /handle/register
Register a new @handle for the authenticated user's account.
Request Body
| Field | Type | Required | Description |
handle | string | required | Handle (3-20 chars, lowercase alphanumeric + underscore) |
displayName | string | optional | Display name (max 50 characters) |
Response (201 Created)
{
"handle": "alice.jil",
"address": "jil1abc123def456...",
"registered_at": "2026-02-09T14:30:00.000Z"
}
Handle Rules: Handles must be 3-20 characters, lowercase alphanumeric with underscores allowed.
Each account can register one handle. Handles are globally unique and cannot be transferred.
Account Recovery
Social recovery enables account access restoration through guardian approvals with a 24-hour
timelock. This protects against unauthorized recovery attempts while ensuring legitimate users
can always regain access.
POST /wallet/recovery/start
Initiate an account recovery ceremony. This begins the social recovery process with a 24-hour timelock. No authentication required (the user has lost access).
Request Body
| Field | Type | Required | Description |
accountId | string | required | Account ID to recover |
newCredentialCommitment | string | required | SHA-256 hash of the new passkey public key |
Response (201 Created)
{
"ceremonyId": "rec_xyz789",
"requiredApprovals": 2,
"currentApprovals": 0,
"guardians": [
{ "id": "grd_001", "name": "Bob", "approved": false },
{ "id": "grd_002", "name": "Carol", "approved": false },
{ "id": "grd_003", "name": "Dave", "approved": false }
],
"timelockEndsAt": "2026-02-10T14:30:00.000Z",
"expiresAt": "2026-02-16T14:30:00.000Z"
}
24-Hour Timelock: Even after all guardian approvals are collected, the recovery cannot
be finalized until the 24-hour timelock expires. This gives the legitimate account holder time to
cancel a fraudulent recovery attempt.
POST /wallet/recovery/approve
Submit a guardian approval for an ongoing recovery ceremony. Each guardian signs with their Ed25519 key.
Request Body
| Field | Type | Required | Description |
ceremonyId | string | required | Recovery ceremony ID |
guardianId | string | required | Guardian's identifier |
guardianSignature | string | required | Ed25519 signature over the ceremony ID + new credential commitment |
Response (200 OK)
{
"ceremonyId": "rec_xyz789",
"approved": true,
"currentApprovals": 1,
"requiredApprovals": 2
}
POST /wallet/recovery/finalize
Finalize recovery after the timelock expires and required approvals are met. Binds the new credential to the account and revokes all previous passkeys.
Request Body
| Field | Type | Required | Description |
ceremonyId | string | required | Recovery ceremony ID |
newCredential | object | required | WebAuthn attestation response for the new passkey |
Response (200 OK)
{
"ceremonyId": "rec_xyz789",
"status": "completed",
"accountId": "acc_abc123",
"newCredentialId": "cred_new_x1y2z3",
"revokedCredentials": 1,
"completedAt": "2026-02-10T14:30:05.000Z"
}
Transaction Proofs
GET /api/proof/tx/{txId}
Get a cryptographic proof receipt for a transaction. Includes the Merkle inclusion proof
and 14-of-20 validator quorum signatures. No authentication required.
Path Parameters
| Parameter | Type | Required | Description |
txId | string | required | Transaction ID (e.g. tx_7f3a9b2c4e1d) |
Response (200 OK)
{
"txId": "tx_7f3a9b2c4e1d",
"receipt": {
"blockHeight": 1000042,
"blockHash": "0x4e7f1a...c3d8b2",
"stateRoot": "0x3c1d8a...f2e7b9",
"merkleProof": ["0xa1...", "0xb2...", "0xc3..."],
"quorumSignatures": {
"threshold": "14-of-20",
"signers": 14,
"signatures": [
{
"validator": "val_de_01",
"jurisdiction": "DE",
"signature": "0xed25519..."
}
]
}
},
"timestamp": "2026-02-09T14:30:02.345Z"
}
Proof Verification: Transaction proofs can be independently verified by
any party using the validator public keys published in the genesis block. The Merkle proof
confirms transaction inclusion, and the quorum signatures confirm validator consensus.
Error Codes
All error responses include a JSON body with error containing code,
message, and optional details fields.
Error Response Format
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Account has insufficient funds for this transaction",
"details": {
"available": "50.00",
"required": "100.00",
"asset": "JIL"
}
}
}
HTTP Status Codes
| Status | Error Code | Description |
400 | VALIDATION_ERROR | Request body failed Zod schema validation. Check details for field-level errors. |
401 | UNAUTHORIZED | Missing, invalid, or expired JWT Bearer token. |
401 | PASSKEY_VERIFICATION_FAILED | WebAuthn assertion verification failed. |
404 | ACCOUNT_NOT_FOUND | Account ID does not exist. |
404 | HANDLE_NOT_FOUND | Handle does not exist in the registry. |
404 | TRANSACTION_NOT_FOUND | Transaction ID does not exist. |
409 | HANDLE_ALREADY_TAKEN | The requested handle is already registered. |
422 | INSUFFICIENT_BALANCE | Account does not have enough funds for the requested transfer. |
423 | TIMELOCK_NOT_EXPIRED | Recovery finalization attempted before 24-hour timelock expired. |
429 | RATE_LIMIT_EXCEEDED | Too many requests. Includes retry_after_ms field. |
Code Examples
Complete JavaScript/TypeScript examples for common wallet operations.
1. Authenticate with Passkey
const optionsRes = await fetch('https://wallet.jilsovereign.com/auth/passkey/authenticate/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'alice@example.com' }),
});
const { publicKey } = await optionsRes.json();
const credential = await navigator.credentials.get({ publicKey });
const verifyRes = await fetch('https://wallet.jilsovereign.com/auth/passkey/authenticate/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
});
const { token } = await verifyRes.json();
2. Send Funds (Two-Phase)
const optionsRes = await fetch('/wallet/send/options', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: '@alice.jil',
asset: 'JIL',
amount: '100.00',
memo: 'Coffee money',
}),
});
const { txIntent, challenge } = await optionsRes.json();
const credential = await navigator.credentials.get({ publicKey: challenge.publicKey });
const submitRes = await fetch('/wallet/send/submit', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ txIntent, credential }),
});
const result = await submitRes.json();
console.log('TX confirmed:', result.txId);
3. Check Balances
const res = await fetch('/wallet/balances', {
headers: { 'Authorization': `Bearer ${token}` },
});
const { assets, protectionTier, protectionCoverage } = await res.json();
assets.forEach(a => {
console.log(`${a.symbol}: ${a.balance} ($${a.usdValue})`);
});
console.log(`Protection: ${protectionTier} ($${protectionCoverage} coverage)`);
4. Resolve an @Handle
const res = await fetch('/handle/resolve/alice.jil');
const { address, profile } = await res.json();
console.log(`@alice.jil resolves to ${address}`);
console.log(`Display name: ${profile.displayName}`);
5. Start Social Recovery
const res = await fetch('/wallet/recovery/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
accountId: 'acc_abc123',
newCredentialCommitment: 'sha256-hash-of-new-pubkey',
}),
});
const ceremony = await res.json();
console.log(`Ceremony: ${ceremony.ceremonyId}`);
console.log(`Need ${ceremony.requiredApprovals} of ${ceremony.guardians.length} guardians`);
console.log(`Timelock ends: ${ceremony.timelockEndsAt}`);