---
title: "Wallet API"
description: "Server actions and HTTP routes for custodial wallets, native-token transfers, fiat credit ledger, and Solana desk quotes — with credit.creditBalanceUnitToMainCurrency as fiat spend SSOT"
locale: "en"
---
# Wallet API

> **Info**
> Filter with **Founder** / **Developer** in the docs sidebar. Prefer **Server Actions** for UI. HTTP routes remain for MCP, webhooks, and non-React clients. Desk oracle rate lives in `platform_settings` / `web3` via `native-token-oracle.ts` — separate from fiat credit accounting.

Ring Platform wallet operations use two layers:

1. **Server Actions** (preferred for React) — `app/_actions/wallet.ts` — type-safe, `revalidatePath`, works with `useActionState`.
2. **API Routes** — `app/api/wallet/**/route.ts` — session-cookie HTTP for external integrations and MCP.

Browser calls use **Auth.js session cookies** on the same origin — see [API reference](/docs/api.md). UI entry: `/wallet` (protected).

### For founders

## What members experience

  
- **[Wallet feature](/docs/features/wallet.md)** — Dashboard, fiat credit points, Token Desk conversion, gasless native sends.

  
- **[Store & credits](/docs/features/store.md)** — Store checkout debits credit points via PaymentConductor `credit_balance` (same fiat rate SSOT).

  
- **[Authentication](/docs/api/authentication.md)** — Social sign-in provisions custodial wallets automatically.

  
- **[Web3 desk (admin)](/docs/api/admin.md)** — Superadmin sets native-token oracle rate and gas reserve — not member-facing.

### Two rates — do not mix them

| Rate | Config | Used for |
|------|--------|----------|
| **Fiat credit accounting** | `ring-config.json` → `credit.creditBalanceUnitToMainCurrency` (usually `1`) | Debiting/crediting **points** vs store `mainCurrency` |
| **Token Desk oracle** | `platform_settings.web3.oracle.nativePerMainCurrency` | Converting **points ↔ native token** on the desk |

> **Tip**
> Keep `credit.creditBalanceUnitToMainCurrency` at `1` unless you intentionally scale points vs fiat. Changing the desk oracle (`nativePerMainCurrency`) never changes store or ad-hoc credit spend math.

### Typical member flows

- **First login** — `ensureUserWallets` / `WalletConductor.ensureNativeWallet` provisions custodial address (Solana or EVM per clone).
- **Check balance** — `listUserWallets` + `getCreditBalance`.
- **Send native tokens** — `transferNativeTokens` (treasury-sponsored gas on Solana).
- **Top up credits (card / PayPal)** — `initiateCreditTopupPayment` → `WalletConductor.initiateTopUp` → PaymentConductor `wallet_topup` → UI `followCheckoutResult(redirect)` → webhook credits ledger. Card uses env processor; PayPal tab sends `processor=paypal`.
- **Top up credits (chain proof)** — `topUpCredits` / `verifyTopUpTransaction`.
- **Pay with credits (ad-hoc)** — `spendCredits` Server Action or `POST /api/wallet/credit/spend`.
- **Pay in store** — checkout rail `credit_balance` (PaymentConductor), not a raw UI call to `/credit/spend`.

> **Warning**
> **Custodial model:** Ring signs transfers server-side for social-auth wallets. External wallets (MetaMask, Coinbase Wallet, WalletConnect via the Wagmi v3 connector picker) connect client-side — they do not use custodial server actions.

### For developers

## Server actions (preferred)

All server actions live in `app/_actions/wallet.ts`. Pattern: auth → service / `WalletConductor` → `revalidatePath`.

### Inventory (high-signal)

| Action | Role |
|--------|------|
| `ensureUserWallets` / `listUserWallets` / `getWalletBalance` | Provision + list custodial wallets |
| `getCreditBalance` / `getCreditHistory` / `getSpendSummary` | Fiat ledger reads |
| `topUpCredits` / `spendCredits` / `transferCredits` | Fiat ledger mutations (`spendCredits` → `WalletConductor.spendCredits`) |
| `initiateCreditTopupPayment` | Thin adapter → `WalletConductor.initiateTopUp` → PaymentConductor `wallet_topup`; returns `{ redirect, paymentUrl?, paymentFields? }` — client must call `followCheckoutResult` |
| `transferNativeTokens` | Gasless native-token send |
| `executeDeskQuote` | Credit ↔ native desk trade |
| `createPinAccessTokenAction` | PIN-gated single-use access token |

### Credit spend rate SSOT

Fiat ledger debits must use the config multiplier — never the desk oracle.

```mermaid
flowchart LR
  CFG["ring-config credit.creditBalanceUnitToMainCurrency"]
  GET["getCreditUnitToMainCurrencyRate()"]
  FIAT["getMainCurrencyCreditAccountingRate()"]
  API["POST /api/wallet/credit/spend"]
  SA["spendCredits Server Action"]
  IC["credit-balance.processor"]
  WC["WalletConductor.spendCredits"]
  CBS["CreditBalanceService.spendCredits"]
  CFG --> GET
  GET --> FIAT
  FIAT --> API
  FIAT --> IC
  SA --> WC
  API --> WC
  WC -->|"usdRate default = fiat"| CBS
  IC --> CBS
```

| Fact | Truth |
|------|--------|
| SSOT | `ring-config.json` → `credit.creditBalanceUnitToMainCurrency` (usually `1`) |
| Helpers | `getCreditUnitToMainCurrencyRate` / `getMainCurrencyCreditAccountingRate` in `lib/ring-config-core.ts` + `lib/payments/credit-balance.ts` |
| Fallback | `exchangeRates[mainCurrency]`, then `1` |
| Ad-hoc debit | `WalletConductor.spendCredits` (Server Action omits `usdRate` → conductor defaults to fiat helper) |
| Store checkout | PaymentConductor `credit_balance` → `credit-balance.processor.ts` (also calls `getMainCurrencyCreditAccountingRate`) |
| Never | `platform_settings.web3.oracle.nativePerMainCurrency` / `getNativeTokenPerMainCurrencyRate` for fiat `usdRate` |
| Desk only | `native-token-oracle` / `creditBalanceUnitPerNativeToken` for points ↔ RING |

> **Warning**
> Document **POST** `/api/wallet/credit/spend` only. A GET stub may return **410** in code — it is not a capability. Use `getSpendSummary` or `GET /api/wallet/credit/history` for reads.

### Action call pattern

```mermaid
sequenceDiagram
  participant C as Client form
  participant A as wallet.ts action
  participant V as WalletConductor / service
  C->>A: FormData
  alt not signed in
    A-->>C: { success: false, error: "Authentication required" }
  else authorized
    A->>V: dynamic import + call
    V-->>A: result
    A->>A: revalidatePath('/[locale]/wallet')
    A-->>C: { success: true, ... }
  end
```

## HTTP routes (external / MCP)

### Wallet lifecycle

| Method | Path | Notes |
|--------|------|-------|
| GET | `/api/wallet/list` | `{ wallets }` via `listWallets()` — `?refresh=true` force on-chain via `refreshBalancesForUser` (publishes `wallet:list`) |
| POST | `/api/wallet/ensure` | Idempotent create-if-missing — `ensureWallets` publishes `wallet:list` when provisioning changes |
| GET | `/api/wallet/balance` | Default wallet native-token balance |

### Native-token operations

| Method | Path | Notes |
|--------|------|-------|
| GET | `/api/wallet/token/balance` | `{ balance, address, chain, symbol }` |
| POST | `/api/wallet/token/transfer` | Custodial native-token send; `wallet_transactions` row |
| POST | `/api/wallet/transfer` | External EVM custodial POL path when `chains.enabled` includes `evm` — not Solana native SSOT |

### Activity

| Method | Path | Notes |
|--------|------|-------|
| GET | `/api/wallet/activity` | `filter=all\|credit\|chain`, unified feed |

### Credit ledger

| Method | Path | Notes |
|--------|------|-------|
| GET | `/api/wallet/credit/balance` | Balance + spend limits — live UI via Tunnel `credit:balance` (`publishToUserTunnel` after ledger mutations) |
| PUT | `/api/wallet/credit/balance` | Admin airdrop `{ user_id, amount, reason }` |
| POST | `/api/wallet/credit/topup` | Optional `tx_hash` verification |
| POST | `/api/wallet/credit/spend` | Direct fiat debit via `WalletConductor.spendCredits` + `creditBalanceUnitToMainCurrency` |
| GET | `/api/wallet/credit/history` | Paginated ledger |

Schemas: `lib/zod/credit-schemas.ts`. Service: `features/wallet/services/credit-balance-service.ts`.

### Solana desk

| Method | Path | Notes |
|--------|------|-------|
| GET | `/api/wallet/desk/quote` | Query `side`, `amount` |
| POST | `/api/wallet/desk/execute` | `{ idempotencyKey, quoteToken }` |

## SSOT file reference

| Concern | File | Key exports |
|---------|------|-------------|
| Credit CRUD | `features/wallet/services/credit-balance-service.ts` | `addCredits`, `spendCredits`, `getCreditHistory` |
| Conductor | `features/wallet/conductor/wallet-conductor.ts` | `spendCredits`, `ensureNativeWallet` |
| Fiat rate | `lib/ring-config-core.ts` / `lib/payments/credit-balance.ts` | `getCreditUnitToMainCurrencyRate`, `getMainCurrencyCreditAccountingRate` |
| Store credit checkout | `lib/payments/processors/credit-balance.processor.ts` | `createInternalCreditCheckout` |
| Gasless transfers | `features/wallet/chains/native-token-transfer-service.ts` | `transferNativeTokenForUser` |
| Desk trades | `features/wallet/chains/solana/desk-service.ts` | `quoteDesk`, `executeDesk` |
| Native oracle | `features/wallet/services/native-token-oracle.ts` | `getNativeTokenPerMainCurrencyRate`, `setNativeTokenPerMainCurrencyRate` |
| Wallet DB | `lib/wallet/user-wallet-db.ts` | `getUserWallets`, `getNativeWallet` |
| PIN tokens | `lib/wallet/pin-access-token-db.ts` | `issueAccessToken`, `consumeAccessToken` |

### Example — spend credits

**Prefer Server Action (React UI)**

Form fields: `amount` (required), `description` (optional — defaults to `Credit spend`), `orderId` (optional). Conductor applies `getMainCurrencyCreditAccountingRate()` when `usdRate` is omitted.

{`import { spendCredits } from '@/app/_actions/wallet'

const result = await spendCredits(formData)
// form fields: amount, description?, orderId?`}

**Or HTTP POST (external / MCP)**

`CreditSpendRequestSchema` requires `amount` and `description`. Body may include optional `order_id`, `reference_id`, `metadata`.

{`curl -X POST "$ORIGIN/api/wallet/credit/spend" \\
  -H "Content-Type: application/json" \\
  -H "Cookie: ..." \\
  -d '{"amount":"10.00","description":"API debit"}'`}

**Store checkout (do not call spend HTTP from store UI)**

PaymentConductor rail `credit_balance` → `credit-balance.processor.ts` → `creditBalanceService.spendCredits` with the same fiat rate helper.

### Environment variables

| Variable | Required | Used by |
|----------|----------|---------|
| `WALLET_ENCRYPTION_KEY` | Yes | AES-256-GCM wallet secrets |
| `SOLANA_RPC_URL` | Yes | Solana RPC |
| `SOLANA_TREASURY_PRIVATE_KEY` | Yes | Sponsored gas / treasury ops |
| `WALLET_ACCESS_TOKEN_TTL_SECONDS` | No (default 900) | PIN access tokens |
| `ORACLE_QUOTE_SECRET` | Desk | Quote HMAC (falls back to `WALLET_ENCRYPTION_KEY`) |
| `RING_ORACLE_DEFAULT_RATE` | No | Desk oracle fallback when DB disabled |
| `PLATFORM_SETTINGS_DISABLE_DB` | No | Env-only oracle reads |

### Desk oracle persistence (native token only)

```mermaid
sequenceDiagram
  participant A as Desk / admin
  participant O as native-token-oracle.ts
  participant D as db()
  participant P as platform_settings/web3
  A->>O: getNativeTokenPerMainCurrencyRate()
  alt PLATFORM_SETTINGS_DISABLE_DB=true
    O-->>A: RING_ORACLE_DEFAULT_RATE
  else DB enabled
    O->>D: readDoc(platform_settings, web3)
    D->>P: oracle.nativePerMainCurrency
    P-->>A: rate
  end
```

Admin write: `setNativeTokenPerMainCurrencyRate` or `POST /api/admin/web3/settings`.

## Related documentation

  
- [features/tunnel-protocol](/docs/features/tunnel-protocol.md) — Depends-on: `credit:balance` and `wallet:list` Tunnel channels that refresh this API

  
- [architecture/wallet-conductor](/docs/architecture/wallet-conductor.md) — Deep-dive: facade methods that publish wallet-list updates after transferNative / ensure.

  
- [features/wallet](/docs/features/wallet.md) — Next-step: member-facing wallet dashboard and credit vs native rails.

  
- [api](/docs/api.md) — See-also: session auth model and domain API map.

  
- [api/authentication](/docs/api/authentication.md) — Prerequisite: crypto wallet provider + social login that provisions custodial wallets.

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

  
- [integrations/ethereum-wallets](/docs/integrations/ethereum-wallets.md) — See-also: Wagmi pathNeedsWeb3 and getWalletBalance live in app/_actions/wallet.ts (dead get-wallet-balance.ts removed).
