---
title: "Wallet & Credit System"
description: "Custodial native-token wallets, fiat credit ledger, gasless Solana transfers, Token Desk credit↔token conversion, and PIN-wrapped encryption on Ring Platform"
locale: "en"
---
# Wallet & Credit System

> **Info**
> Filter with **Founder** / **Developer** in the docs sidebar. This page reflects the verified v1.6.4 codebase: PIN-wrapped encryption, atomic single-write provisioning, `wallet_access_tokens`, and oracle/platform settings persisted via `db()` (no private `pg.Pool`).

Ring Platform's wallet system provides two complementary layers:

1. **Custodial native-token wallets** — auto-provisioned for signed-in members on all enabled chains (Solana first, EVM/Base enabled per clone config). Private keys encrypted with `WALLET_ENCRYPTION_KEY` using AES-256-GCM + scrypt. PIN-wrapped v2 encryption for withdrawal operations.
2. **In-app credit ledger** — fiat USD credits stored in `credit_balance` field on user documents. Used for store purchases, subscriptions, and conversions to native tokens via the Token Desk widget.

Browser routes: `/wallet` (dashboard), `/wallet/topup`, `/wallet/send`, `/wallet/staking`.

## Money layers (SSOT)

Four separate money paths — do not conflate them:

| # | Path | Entry | Result |
|---|------|-------|--------|
| 1 | Card top-up (`wallet_topup`) | `/wallet/topup` / `CreditAddFsModal` → PaymentConductor | **Credit points** (fiat ledger) — never on-chain RING |
| 2 | Token Desk (buy) | `/wallet` Desk widget → `desk-service.ts` (subscriber+) | Credit points → **native RING** (`nativeOut = points / nativePerMainCurrency`) |
| 3 | Store `native_token` rail | `POST /api/store/payments/token` | Spend **native RING** at checkout (separate from top-up) |
| 4 | Chain-proof credit add | `POST /api/wallet/credit/topup` (tx-hash verify) | Credit add after on-chain transfer proof |
| 5 | BuyNativeViaCard (`native_token_onramp`) | Confidential tabs when `CONFIDENTIAL_TOKEN_ONRAMP=true` | Card/PayPal → **treasury RING** (not credit) |

> **Tip**
> Any signed-in user can buy **credit points** with a card (path 1). Converting points → RING is Token Desk (path 2), available to **subscriber+**. Optional `CONFIDENTIAL_TOKEN_ONRAMP=true` enables **BuyNativeViaCard** (`native_token_onramp`) for confidential / admin / superadmin — card or PayPal tabs that settle native RING from treasury (not credit points).

### For founders

## What members experience

  
- **[Wallet dashboard — /wallet](/docs/features/wallet.md)** — Balance hero showing credit balance + native-token balances per chain. Desk widget for converting credits ↔ native tokens.

  
- **[Top-up — /wallet/topup](/docs/features/wallet.md)** — Add credits via native-token transfer (tx-hash verify) or live Card / PayPal (`CreditAddFsModal` → PaymentConductor `wallet_topup`). Card uses env processor (WayForPay or Stripe); PayPal tab sends `processor=paypal`.

  
- **[Send tokens — /wallet/send](/docs/features/wallet.md)** — Gasless transfers to contacts — treasury sponsors transaction fees. Pick recipient from contacts or enter address.

  
- **[Store & credits](/docs/features/store.md)** — Credit spend at checkout — ledger debits before on-chain settlement in some flows.

### Core concepts for clone operators

**Credit balance is fiat points** denominated in store `mainCurrency` (USD on ring-platform.org). Accounting multiplier is `credit.creditBalanceUnitToMainCurrency` in `ring-config.json` (usually `1` = 1:1). Members earn or top up credits; convert to native token only through the Token Desk (separate **oracle** rate in `platform_settings.web3.oracle.nativePerMainCurrency`).

**Native token transfers are gasless.** The clone treasury sponsors Solana transaction fees so members never need SOL for gas. An optional transfer tax can be configured per clone for high-frequency send projects.

**Wallet provisioning is automatic.** When a member signs in (Google, Apple, or credentials), the system provisions wallets for all enabled chains via a single atomic `setUserWallets()` write — no JSONB race, max one wallet per chain. No manual setup required.

**PIN-wrapped encryption** (2026-07-03): Wallet private keys are encrypted with AES-256-GCM at rest. Members who set a 4-digit PIN get their keys wrapped with PIN-derived entropy — the PIN is verified server-side before any withdrawal operation, never sent to the client in plaintext. Keys stored before PIN setup remain in v1 format (env-only encryption) and are lazily re-encrypted on first PIN-based operation.

### Typical member flows

- **First login** — wallet auto-provisioned; credit balance initialized at 0.
- **Check balance** — wallet page shows credit balance + native-token balance per chain. Credits push live on Tunnel `credit:balance`; custodial wallet rows refresh on `wallet:list` when Tunnel is connected (60s poll only as fallback).
- **Send tokens to a contact** — pick recipient from contacts, enter amount, confirm. Gas sponsored by treasury.
- **Top up credits** — native-token transfer (tx hash verify) **or** Add Credit modal (`CreditAddFsModal`): **Card** | **PayPal** tabs → `WalletConductor.initiateTopUp` → PaymentConductor (amount 25–2000). Browser follows `redirect` via `followCheckoutResult`. Balance updates after webhook **Approved** / capture — not when the browser returns to `/wallet`.

> **Tip**
> Switching a clone’s card rail to Stripe: set `PAYMENT_WALLET_TOPUP_PROCESSOR=stripe` (or `PAYMENT_DEFAULT_PROCESSOR=stripe`). The Card tab stays the same — it does not hardcode WayForPay.

- **Set a wallet PIN** — profile/security page; 4-digit PIN activates v2 PIN-wrapped encryption for all existing wallets.
- **Authorize a withdrawal** — enter PIN; server validates against the stored encryption envelope; single-use access token issued.
- **Convert credits to tokens** — use the Desk widget: choose buy/sell direction, get a quote, execute at oracle rate.
- **Pay in store** — checkout rail `credit_balance` debits the fiat ledger (same `creditBalanceUnitToMainCurrency` rate as ad-hoc spend).
- **Pay with credits (ad-hoc)** — `spendCredits` / `POST /api/wallet/credit/spend` for non-checkout debits (services, membership fee paths).

> **Warning**
> **Custodial model:** Ring signs transfers server-side for auto-provisioned wallets. External wallets (MetaMask, Coinbase Wallet, WalletConnect via the Wagmi v3 connector picker) connect client-side — they do not use custodial transfer routes. Credits and native-token balances are separate concepts.

## Related documentation

  
- [features/tunnel-protocol](/docs/features/tunnel-protocol.md) — Depends-on: live `credit:balance` and `wallet:list` updates while the wallet tab stays open.

  
- [api/wallet](/docs/api/wallet.md) — Deep-dive: Server Actions and HTTP SSOT for list, credit, and transfer.

  
- [customization/token-economics](/docs/customization/token-economics.md) — See-also: oracle rate, transfer tax, first-settler discounts.

  
- [features/store](/docs/features/store.md) — Same-workflow: credit spend at checkout.

  
- [features/authentication](/docs/features/authentication.md) — Prerequisite: Google/Apple/social sign-in triggers wallet provisioning.

  
- [integrations/ethereum-wallets](/docs/integrations/ethereum-wallets.md) — Deep-dive: Wagmi v3 pathNeedsWeb3 gate, getEvmRpcUrl / getEvmTokenAddress SSOT.

  
- [features/public-pools](/docs/features/public-pools.md) — Same-workflow: desk oracle nativePerMainCurrency converts public-pool card chip-ins to pledged native (not wallet_topup 1:1).

  
- [features/ring-oracle](/docs/features/ring-oracle.md) — Depends-on: Ring Oracle facade for desk quotes, FX overlay, and credit accounting rates.

### For developers

## Architecture

The wallet system uses **server actions** (`app/_actions/wallet.ts`) as the primary integration layer. These actions call SSOT service files in `features/wallet/` and `lib/wallet/`.

### Server actions inventory

| # | Action | Layer | Purpose |
|---|--------|-------|---------|
| 1 | `ensureUserWallets` | Wallet | Provisions wallets for all enabled chains (atomic single-write) |
| 2 | `listUserWallets` | Wallet | Lists wallets with on-chain balances |
| 3 | `getWalletBalance` | Wallet | Native-token balance via viem/Solana RPC |
| 4 | `getWalletActivity` | Activity | Unified feed: credit + chain transactions |
| 5 | `getCreditHistory` | Credit | Paginated credit transaction history |
| 6 | `getCreditBalance` | Credit | Current credit balance (always fiat USD) |
| 7 | `topUpCredits` | Credit | Add credits after on-chain verification |
| 8 | `spendCredits` | Credit | Ad-hoc fiat debit via `WalletConductor.spendCredits` (rate = `credit.creditBalanceUnitToMainCurrency`, not desk oracle) |
| 9 | `transferCredits` | Credit | Admin credit adjustment |
| 10 | `processMembershipFee` | Credit | Monthly membership fee from credits |
| 11 | `transferNativeTokens` | Token | Gasless native-token transfer (treasury-sponsored) |
| 12 | `getNativeTokenPerMainCurrencyRate` | Oracle | Current oracle rate |
| 13 | `setNativeTokenPerMainCurrencyRate` | Oracle | Update oracle rate (admin-only) |
| 14 | `signDeskQuote` | Desk | Sign quote for credit↔token conversion |
| 15 | `executeDeskQuote` | Desk | Execute signed quote (buy/sell/refund) |
| 16 | `verifyDeskQuote` | Desk | Verify signed quote token |
| 17 | `verifyTopUpTransaction` | Verify | Verify on-chain ERC20/SPL transfer |
| 18 | `listWalletTransactions` | Activity | Read wallet_transactions collection directly |
| 19 | `setPrimaryWallet` | Wallet | Set default wallet address |
| 20 | `getSpendSummary` | Credit | Period-based credit spending summary |
| 21 | `getRewardCreditAddEventSummary` | Credit | Credit rewards summary for user |
| 22 | `createPinAccessTokenAction` | Security | Issue single-use PIN-gated access token (React 19 `useActionState`) |
| 23 | `revokePinAccessTokensAction` | Security | Revoke all active PIN access tokens for current user |
| 24 | `initiateCreditTopupPayment` | Credit / Payments | WayForPay card top-up via PaymentConductor `wallet_topup` (amount 25–2000) |

### Integration pattern

Server actions follow the SSOT pattern: dynamic import of service → auth check → operation → `revalidatePath('/[locale]/wallet')` for cache invalidation.

**Import and call a server action**

Server actions can be called directly from client components using React 19's `useActionState` hook.

{`import { useActionState } from 'react'
import { topUpCredits } from '@/app/_actions/wallet'

const [state, formAction, isPending] = useActionState(
  async (prevState, formData) => {
    const result = await topUpCredits(formData)
    if (result.success) await refreshBalance()
    return result
  },
  null
)`}

**Or use with form progressive enhancement**

{`
  
  
  
  
    {isPending ? 'Processing...' : 'Top Up'}
  
`}

### SSOT service files

| Concern | File | Key exports |
|---------|------|-------------|
| Credit CRUD | `features/wallet/services/credit-balance-service.ts` | `addCredits`, `spendCredits`, `addFiatUsd`, `spendFiatUsd`, `getCreditHistory`, `hasSufficientBalance` |
| Gasless transfers | `features/wallet/chains/native-token-transfer-service.ts` | `transferNativeTokenForUser(userId, toAddress, amount)` — treasury sponsors gas |
| Treasury ops | `features/wallet/chains/solana/treasury-transfer-service.ts` | `transferNativeTokenFromTreasury`, `transferNativeTokenToTreasury`, `burnRingFromUser` |
| Desk trades | `features/wallet/chains/solana/desk-service.ts` | `quoteDesk`, `executeDesk` — credit↔token conversion |
| Oracle rate | `features/wallet/services/native-token-oracle.ts` | `getNativeTokenPerMainCurrencyRate`, `setNativeTokenPerMainCurrencyRate` — `platform_settings` doc `web3` via `db().readDoc` / `updateDoc` |
| Wallet DB | `lib/wallet/user-wallet-db.ts` | `getNativeWallet`, `getUserWallets`, `setUserWallets`, `migrateUserWalletsToPin` |
| Wallet provisioning | `features/wallet/conductor/wallet-conductor.ts` + `ensure-wallet.ts` | App callers use `WalletConductor.ensureNativeWallet`; low-level `ensureWallets` remains the atomic multi-chain writer |
| Balance fetch | `features/wallet/services/list-wallets.ts` | `listWallets` — gets on-chain balances via viem/Solana RPC |
| Activity feed | `features/wallet/services/wallet-activity-feed.ts` | `getWalletActivityFeed` — unified credit+chain feed with filters |
| Top-up verify | `features/wallet/services/topup-verification.ts` | `verifyTopUpTransaction`, `reserveTopUpTxHash` |
| Encryption | `lib/wallet/encrypt-wallet-secret.ts` | `encryptWalletSecret`, `decryptSecretWithPin`, `reencryptLegacyWallet` (v1→v2 migration) |
| Wallet decrypt | `lib/wallet/decrypt-user-wallet.ts` | `decryptUserWalletPrivateKey`, `decryptSolanaWalletSecretKey` (Uint8Array), `decryptSolanaWalletSecretKeyWithPin` |
| PIN access tokens | `lib/wallet/pin-access-token-db.ts` | `issueAccessToken`, `consumeAccessToken`, `revokeActiveTokens` |
| Credit schemas | `lib/zod/credit-schemas.ts` | `CreditTopUpRequestSchema`, `CreditSpendRequestSchema`, `CreditTransactionType` |
| Desk schemas | `lib/zod/desk-schemas.ts` | `DeskExecuteRequestSchema`, `DeskQuoteRequestSchema` |

### Wallet pages (verified routes)

| Route | Component | Description |
|-------|-----------|-------------|
| `/wallet` | `wallet-client.tsx` | Balance hero (credit + native-token), DeskWidget, transaction feed |
| `/wallet/topup` | `topup-client.tsx` | Native-token transfer + live WayForPay card (`initiateCreditTopupPayment`) |
| `/wallet/send` | `send-tokens.tsx` | Gasless transfers to contacts |

### PIN-gated access flow

```mermaid
sequenceDiagram
  participant C as Client (wallet page)
  participant SA as Server Action
  participant EW as WalletConductor.ensureNativeWallet()
  participant P as pin-access-token-db
  participant DB as wallet_access_tokens

  C->>SA: createPinAccessTokenAction({ pin, scope })
  SA->>SA: auth() check
  SA->>EW: ensure native wallet
  SA->>P: issueAccessToken(userId, wallet, pin)
  P->>P: decryptSecretWithPin() — PIN verify
  alt PIN wrong
    P-->>SA: throw
    SA-->>C: { error: "Incorrect PIN" }
  else v1 (legacy) wallet
    P-->>SA: throw "migrate"
    SA-->>C: { error: "Migrate wallet to set a PIN", requiresMigration: true }
  else PIN correct
    P->>DB: store sha256(token) + expiresAt
    P->>DB: revoke prior active tokens for (userId, scope)
    P-->>SA: { accessToken, walletAddress, expiresAt }
    SA-->>C: { success, accessToken, walletAddress, expiresAt }
  end
```

#### Encryption envelope format (v2)

Wallets created after the 2026-07-03 refactor use the versioned v2 format:

```
v2::::[:]
```

- **No PIN**: `v2:scryptSalt:iv:encrypted:authTag` — key derived from `WALLET_ENCRYPTION_KEY` + random salt.
- **With PIN**: same + `:sha256(pin)` appended — key derived from `WALLET_ENCRYPTION_KEY + ":" + sha256(pin)` + random salt.
- **Fast reject**: stored `pinHash` enables constant-time PIN rejection before running expensive scrypt (DoS defense).
- **Migration**: `migrateUserWalletsToPin(userId, pin)` re-encrypts all legacy v1 wallets for a user.

Legacy v1 wallets (`iv:encrypted:authTag`) remain readable via `decryptWalletSecretLegacy()`.

#### Access token properties

- **Storage**: `wallet_access_tokens` table (JSONB, sha256-hashed, never raw token persisted)
- **TTL**: 15 minutes (configurable via `WALLET_ACCESS_TOKEN_TTL_SECONDS` env var)
- **Single-use**: consumed atomically via `usedAt` timestamp after successful validation
- **Auto-revoke**: issuing a new token for the same `(userId, scope)` revokes all prior active tokens — prevents replay
- **Audit**: every issuance creates a `wallet_transactions` row with type `pin_access_granted`

### Desk trade flow (credit ↔ token conversion)

```mermaid
sequenceDiagram
  participant C as Client (wallet page)
  participant A as Server Action
  participant O as Oracle
  participant S as Desk Service
  participant T as Treasury (Solana)
  participant D as DB (credits)

  C->>A: signDeskQuote({ side: buy, amount })
  A->>O: getNativeTokenPerMainCurrencyRate()
  O-->>A: rate
  A-->>C: quoteToken + ringAmountUi
  C->>A: executeDeskQuote({ idempotencyKey, quoteToken })
  A->>S: executeDesk(userId, idempotencyKey, quoteToken)
  alt buy (USD → native token)
    S->>D: spendFiatUsd (debit credits)
    S->>T: transferNativeTokenFromTreasury (send native token)
  else sell (native token → USD)
    S->>T: transferNativeTokenToTreasury + burnRingFromUser
    S->>D: addFiatUsd (credit USD)
  end
  S-->>A: { orderId, txHash }
  A-->>C: { success, txHash }
```

### WalletChain — SSOT identity model

The chain identity model was consolidated in the 2026-07-03 refactor:

| Type | Definition | Purpose |
|------|-----------|---------|
| `NativeChain` | `(typeof ringConfig.chains.native)` | Single chain identity (`'solana'` on ring-platform.org) |
| `SupportedChains` | `(typeof ringConfig.chains.supported)[number]` | All chains the clone supports (`'solana' \| 'evm' \| 'base'`) |
| `EnabledChains` | `(typeof ringConfig.chains.enabled)[number]` | Subset of supported chains actively in use |
| `WalletChain` | `EnabledChains` | Chains a user can hold a wallet for (widened from `NativeChain` for story 5) |
| `DEFAULT_WALLET_CHAIN` | `'evm'` | Fallback for legacy chainless wallet rows (pre-Solana EVM era) |

All three previously scattered hardcoded fallbacks (`'solana'` in utils.ts, `'evm'` in user-wallet-db.ts, `'native'` bug in ensure-wallet.ts) now point to `DEFAULT_WALLET_CHAIN`.

### Credit schema (Zod SSOT)

All credit operations use `lib/zod/credit-schemas.ts`. Valid transaction types:
`payment`, `airdrop`, `reimbursement`, `purchase`, `membership_fee`, `top_up`, `bonus`, `penalty`, `desk_buy`, `desk_sell`, `desk_refund`.

Credit balance is **always fiat USD** on ring-platform.org — never native-token denomination:

{`// lib/zod/credit-schemas.ts
export const UserCreditBalanceSchema = z.object({
  amount: z.string(),       // Fiat balance amount (USD)
  main_currency_equivalent: z.string(),
  fiat_currency: z.string().optional(), // 'USD' on ring-platform.org
  last_updated: z.number(),
  subscription_active: z.boolean().default(false),
})`}

### Conversion formula

The Token Desk converts at the oracle rate (`desk-service.ts`):
```
nativeOut = (points × pointFiatValue) / nativePerMainCurrency   // pointFiatValue = 1
100 credit points (USD) at nativePerMainCurrency=100 → 1 native token
```

Rate stored in `platform_settings` collection, document id `web3`, field path `oracle.nativePerMainCurrency`. `native-token-oracle.ts` uses `db()` from `@/lib/database` — same persistence layer as `platform-settings-service.ts`. When `PLATFORM_SETTINGS_DISABLE_DB=true`, reads use `RING_ORACLE_DEFAULT_RATE` (fallback `100`) and writes throw. Superadmin updates via `setNativeTokenPerMainCurrencyRate` action or `POST /api/admin/web3/settings`.

### Environment variables

| Variable | Required | Used by |
|----------|----------|---------|
| `WALLET_ENCRYPTION_KEY` | Yes | `encrypt-wallet-secret.ts` — AES-256-GCM encryption of private keys |
| `SOLANA_RPC_URL` | Yes | `solana-client.ts` — on-chain Solana operations |
| `SOLANA_TREASURY_PRIVATE_KEY` | Yes | `treasury-transfer-service.ts` — sponsored gas transfers |
| `WALLET_ACCESS_TOKEN_TTL_SECONDS` | No (default: 900) | `pin-access-token-db.ts` — single-use token TTL |
| `POLYGON_RPC_URL` / `getEvmRpcUrl()` | For EVM chains | `lib/ring-config-chain.ts` + `features/wallet/chains/evm/evm-token-transfer.ts`; live balance via `getWalletBalance` in `app/_actions/wallet.ts` |
| `ORACLE_QUOTE_SECRET` | For desk | `native-token-oracle.ts` — desk quote HMAC signing (falls back to `WALLET_ENCRYPTION_KEY`) |
| `RING_ORACLE_DEFAULT_RATE` | No (default `100`) | Oracle read fallback when DB row missing or `PLATFORM_SETTINGS_DISABLE_DB=true` |
| `PLATFORM_SETTINGS_DISABLE_DB` | No | When `true`, oracle/settings reads use env defaults; writes blocked |
| `CONFIDENTIAL_TOKEN_ONRAMP` | No (default `false`) | PaymentConductor `native_token_onramp` — confidential+ card/PayPal → treasury RING |
| `NEXT_PUBLIC_CONFIDENTIAL_TOKEN_ONRAMP` | No (default `false`) | Client UI mirror for onramp tabs |

See `lib/wallet/encrypt-wallet-secret.ts` for the v2 envelope format cryptographic spec.

### Atomic provisioning (race fix)

`ensureWallets()` provisions all missing chain wallets into an **in-memory Map** keyed by chain, then persists via **one** `setUserWallets()` write. This eliminates the JSONB read-modify-write race that previous parallel `Promise.all(provisionChainWallet())` designs produced, and guarantees the "max one wallet per enabled chain" invariant (`appendWalletIfMissing` enforces per-chain idempotency).

### Card credit top-up (live) vs native-token rail

`WalletConductor.initiateTopUp` / `initiateCreditTopupPayment` → `PaymentConductor.createCheckout({ purpose: 'wallet_topup' })` → processor from env or form `processor` → `normalizeCheckoutResult` → UI `followCheckoutResult` (`lib/payments/checkout-redirect.ts`).

| Tab (`CreditAddFsModal`) | Form | Processor |
|--------------------------|------|-----------|
| **Card** | No `processor` field | `PAYMENT_WALLET_TOPUP_PROCESSOR` / `PAYMENT_DEFAULT_PROCESSOR` (WayForPay HPP `form_post` or Stripe `navigate`) |
| **PayPal** | `processor=paypal` | PayPal Orders v2 approve URL (`navigate`) when credentials configured |

Webhook → `handlers/wallet-topup.ts` / `wallet-topup-stripe.ts` / `wallet-topup-paypal.ts` → `creditBalanceService.addFiatUsd` (1:1 fiat points). Amount gate: 25–2000. Card top-up credits the **fiat ledger only** — it never mints on-chain RING. Entry UI: `/wallet/topup` and `features/wallet/components/credit-add-fs-modal.tsx`.

Deep HPP / returnUrl vs serviceUrl: [WayForPay](/docs/features/wayforpay-integration.md) · [PaymentConductor architecture](/docs/architecture/payment-conductor.md).

To spend native RING directly, use the store `native_token` rail (`POST /api/store/payments/token`) — a distinct path from top-up. See [PaymentConductor](/docs/features/payment-conductor.md).

### Chain-proof credit add — `POST /api/wallet/credit/topup`

Distinct from PaymentConductor card top-up. Verifies an on-chain transfer to treasury: when `isChainProofRequired()`, the request must include a `tx_hash` which is reserved (`reserveTopUpTxHash`, anti-double-spend) and verified (`verifyTopUpTransaction`) before `creditBalanceService.addCredits`. Non-`top_up` transaction types (airdrop/bonus) require platform-admin role.

### WalletConductor (facade — adopted)

`features/wallet/conductor/wallet-conductor.ts` is the SSOT orchestration layer for custodial native-token web3 + credit money paths. Thin adapters (`app/_actions/wallet.ts`, `/api/wallet/token/*`, `/api/wallet/desk/*`) call the conductor; it owns PaymentConductor checkouts for `wallet_topup` / `native_token_onramp`, Token Desk, send, and user credit-spend. Legacy `/api/wallet/ring/*` aliases are removed — use `/api/wallet/token/*`.

| Method | Purpose |
|--------|---------|
| `ensureNativeWallet(userOverride)` | Provision native + enabled-chain wallets; returns `{ ok, native, wallets }` (OAuth / override-safe; no session required) |
| `ensureFunded(minCredits)` | Session-gated: provision via `ensureNativeWallet`, assert minimum credit balance |
| `initiateTopUp` | Card / PayPal → credit points (`wallet_topup`); returns Conductor `redirect` |
| `initiateNativeOnramp` | Confidential+ card/PayPal → treasury native (`native_token_onramp`; gated by `isNativeTokenOnrampEnabled`) |
| `quoteDesk` / `executeDesk` | Credit points → native RING (subscriber+; Solana desk) |
| `getNativeBalance` | Custodial native balance |
| `transferNative` | Gasless custodial native send (`chains.native` / `chains.enabled`) |
| `spendCredits` | User credit-spend API / FormData path — fiat rate from `credit.creditBalanceUnitToMainCurrency` (not desk oracle) |

**Not in WalletConductor:** external EVM wallet USDT/POL via `POST /api/wallet/transfer` (SupportedCrypto when `chains.enabled` includes `evm`) — keep separate from Solana custodial SSOT.

### Fiat credit spend — two-rate boundary

> **Tip**
> **Fiat accounting** (`getMainCurrencyCreditAccountingRate` → `credit.creditBalanceUnitToMainCurrency`) applies to ledger debits. **Desk oracle** (`nativePerMainCurrency`) applies only to credit ↔ native conversion. Mixing them understates or overstates store/ad-hoc spends.

| Path | Entry | Rate helper |
|------|-------|-------------|
| Ad-hoc debit | `spendCredits` action / `POST /api/wallet/credit/spend` → `WalletConductor.spendCredits` | `getMainCurrencyCreditAccountingRate()` |
| Store checkout | PaymentConductor `credit_balance` → `credit-balance.processor.ts` | `getMainCurrencyCreditAccountingRate()` |
| Desk buy/sell | `quoteDesk` / `executeDesk` | `getNativeTokenPerMainCurrencyRate()` only |

Deep HTTP tables and Mermaid: [Wallet API](/docs/api/wallet.md).

## Related documentation

  
- **[WalletConductor](/docs/features/wallet-conductor.md)** — SSOT facade for top-up, desk, custodial send, credit spend, NFT buy.

  
- **[Wallet API reference](/docs/api/wallet.md)** — Complete server action signatures and response types.

  
- **[Authentication](/docs/features/authentication.md)** — Social sign-in triggers wallet provisioning.

  
- **[Store feature](/docs/features/store.md)** — Credit spend at checkout.

  
- **[Token economics](/docs/customization/token-economics.md)** — Oracle rate, transfer tax, first-settler discounts.
