---
title: Payment API
tags: [api, payment, reference, request-network, escrow]
---

# Payment API

> **Last updated:** 2026-05-31 — Postgres integration promotion, oracle quote persistence, AMN scanner rail-switch fix, capped webhook confirmation persistence, seller/template payment rail options, and partial gasless permit endpoints.

The payment surface is split across provider-neutral payment routers, Request Network checkout/webhook routes, derived-destination custody routes, and admin safety routes:

| Path prefix | File | Purpose |
| --- | --- | --- |
| `/api/payment/*` | [`paymentControllerRoutes.ts`](../../backend/src/services/payment/paymentControllerRoutes.ts) | New controller pattern (CRUD + configuration) |
| `/api/payment/*` | [`paymentRoutes.ts`](../../backend/src/services/payment/paymentRoutes.ts) | Additional legacy endpoints (tx fetch, exports) |
| `/api/payment/request-network/*` | [`requestNetwork/requestNetworkRoutes.ts`](../../backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts) | Request Network intent creation, in-house checkout payloads, webhook processing |
| `/api/payment/derived-destinations/*` | [`wallets/derivedDestinationRoutes.ts`](../../backend/src/services/payment/wallets/derivedDestinationRoutes.ts) | Derived destination inspection, balance checks, and sweeping |
| `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | Legacy wallet-direct confirmations |
| `/api/payment/amn-scanner/*` | [`routes/amnScannerWebhookRoutes.ts`](../../backend/src/routes/amnScannerWebhookRoutes.ts) | AMN Pay Scanner webhook receiver |
| `/api/admin/rn/networks/*` | [`requestNetwork/networkRegistryRoutes.ts`](../../backend/src/services/payment/requestNetwork/networkRegistryRoutes.ts) | Request Network chain/token registry |
| `/api/admin/payments/awaiting-confirmation/*` | `awaitingConfirmationRoutes.ts` | Admin queue for payments waiting on confirmation/safety checks |

Core model: [[Payment]]. Coordination logic to avoid race conditions when multiple sources update the same payment is in `paymentCoordinator.ts`.

> [!warning] Persistence status
> Payment APIs still create/read/update Mongo `Payment` documents on backend `2.6.83`. The Postgres branch adds schemas, repos, migrations, and optional quote persistence, but it is not a full payment-domain cutover. `/api/payment/request-network/intents` can write `payment_quotes` only when `ORACLE_QUOTING_ENABLED=true`; the payment record itself remains Mongo-backed unless future service wiring changes that boundary.

## Configuration / health

### POST /api/payment/configuration

**Description:** Returns the active payment provider configuration, including Request Network settings, supported chain/token data, receiver/derived-destination context, and redirect/webhook URLs where applicable.
**Auth required:** No
**Request body:** `{ amount?, currency?, purchaseRequestId? }` (used to scope returned config)
**Response 200:**
```json
{
  "accept": [{ "blockchain": "bsc", "token": "0x55d3...", "receiver": "0xa30..." }],
  "redirect": { "success": "...", "cancel": "..." },
  "webhook": "https://.../api/payment/request-network/webhook"
}
```

### GET /api/payment/health

**Description:** Lightweight health probe.
**Auth required:** No
**Response 200:** `{ success, message, endpoints }`. Older builds may still list legacy endpoint names in this health payload; rely on `app.ts` mounts for the authoritative live surface.

### GET /api/payment/shkeeper/config

**Description:** Historical compatibility endpoint for the old SHKeeper-hosted widget. It is not part of the current Request Network checkout path.
**Auth required:** No

## Payment records (CRUD)

### POST /api/payment

**Description:** Create a payment record manually. Normal buyer checkout should use `POST /api/payment/request-network/pay-in`.
**Auth required:** Bearer JWT
**Request body:**
```ts
{
  purchaseRequestId: string;
  sellerOfferId: string;
  buyerId: string;
  sellerId: string;
  amount: { amount: number; currency: string };
  blockchain?: { network: string; token: string };
  metadata?: Record<string, unknown>;
}
```
**Response 201:** `{ /* Payment document */ }`

### PUT /api/payment/:id

**Description:** Update a payment record (status, transactionHash, metadata).
**Auth required:** Bearer JWT
**Request body:**
```ts
{
  status?: "pending" | "processing" | "completed" | "failed" | "cancelled";
  transactionHash?: string;
  blockchain?: { ... };
  metadata?: Record<string, unknown>;
}
```

### GET /api/payment

**Description:** List the caller's payments (defaults to `completed,success` status).
**Auth required:** Bearer JWT
**Query params:** `status`, `limit` (default 50), `offset` (default 0)

### GET /api/payment/:id

**Description:** Fetch a payment by id. For payments with `provider: 'request.network'` that are still `pending`, this endpoint also performs an **on-demand RN reconcile**: it queries the Request Network node live, and if RN reports the request as paid it immediately marks the payment `completed`, advances the purchase request to `processing`, persists `selectedOfferId`, and accepts the winning offer while rejecting all others. This reconcile path exists because RN webhooks cannot reach a local dev server and the reconcile cron is not started there; the same logic fires in production as a safety net.
**Auth required:** Bearer JWT
**Errors:** `404` not found.

> ⚠️ **NOT IMPLEMENTED:** `GET /payment/:id/status`, `POST /payment/:id/confirm`, and `DELETE /payment/:id` do not exist in the codebase. Do not call these paths.

### GET /api/payment/user/:userId

**Description:** Payments for a specific user (admin or self).
**Auth required:** Bearer JWT
**Query params:** `status`, `limit`, `offset`

### GET /api/payment/stats / GET /api/payment/stats/:userId

**Description:** Aggregated counts and sums per status.
**Auth required:** Bearer JWT
**⚠️ Known undercounting:** Only payments with status `'confirmed'` are counted as `successfulPayments`. Payments with status `'completed'` (the terminal state for SHKeeper and DePay) are **not** included in this count and are therefore under-reported.

### GET /api/payment/export / GET /api/payment/export/:userId

**Description:** Export payments as `json` or `csv`.
**Auth required:** Bearer JWT
**Query params:** `format=json|csv`
**⚠️ Privilege gap:** The controller-pattern route for this endpoint has no admin guard. Any authenticated user (not just admins) can export payment data.

> ⚠️ **NOT IMPLEMENTED:** `/payment/history`, `/payment/methods`, `/payment/validate`, `/payment/transactions`, and `/payment/escrow/balance` do not exist. Do not call these paths.

### POST /api/payment/payments/cleanup-pending

**Description:** Admin cleanup of stale `pending` payments.
**Auth required:** Bearer JWT (admin)
**Response 200:** `{ success, deletedCount, message }`

### POST /api/payment/payments/:id/fetch-tx

**Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment.
**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix).
**Response 200:** `{ success, transactionHash, network, source, message }`

### POST /api/payment/payments/auto-fetch-missing

**Description:** Batch tx-hash backfill across the database.
**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix).
**Request body:** `{ limit?: number }` (default 10)

### GET /api/payment/payments/:id/debug

**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. Intended for admin / development.
**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix).

### POST /api/payment/callback

**Description:** Generic payment callback (called by the older client SDK).
**Auth required:** No (verified by `paymentRef` matching)
**Request body:** `{ paymentId, transactionHash, status, data }`

### POST /api/payment/verify

**Description:** Legacy frontend verification endpoint used by the wallet-direct Web3 flow. Confirms a payment and updates the related [[PurchaseRequest]].
**Auth required:** Bearer JWT

## Request Network - Pay-in

### POST /api/payment/request-network/pay-in

**Description:** Creates a plain Request Network pay-in intent and stores a [[Payment]] with `provider: "request.network"`. The service can attach a per-payment derived destination before creating the provider request. This route stays available at `/api/payment/request-network/pay-in`, but new provider-selection checkout integrations should prefer `/api/payment/request-network/intents`.
**Auth required:** Bearer JWT (buyer)
**Request body:**
```ts
{
  purchaseRequestId: string;
  sellerOfferId: string;
  sellerId: string;
  amount: number;
  token?: string;     // default "USDT" or REQUEST_NETWORK_PAYMENT_CURRENCY
  network?: string;   // default REQUEST_NETWORK_NETWORK or "bsc"
  metadata?: Record<string, unknown>;
}
```
**Response 200:** `{ success: true, data: { paymentId, paymentUrl, providerPaymentId, raw, ... } }`

### GET /api/payment/request-network/options

**Description:** Resolves the chain/token rails a buyer may use for a seller or template checkout. Precedence is template override (`RequestTemplate.paymentConfig.useShopDefault === false`), then store defaults (`ShopSettings.paymentConfig`), then the global supported default.
**Auth required:** Bearer JWT
**Query params:** `sellerId?`, `templateId?`
**Response 200:**
```ts
{
  success: true;
  data: {
    allowedChains: number[];
    allowedTokens: string[];
    source: "item" | "store" | "default";
    chains: Array<{
      chainId: number;
      name: string;
      shortName: string;
      tokens: Array<{ symbol: string; address: string; decimals: number }>;
    }>;
  };
}
```
**Frontend use:** Template checkout calls this with both `sellerId` and the real `templateId` before creating payment intents, then defaults to BSC/USDT when allowed or the first returned rail otherwise.

### POST /api/payment/request-network/intents

**Description:** Richer buyer intent endpoint used by the provider-selection checkout. It can dispatch either to `request.network` or `amn.scanner`, validates the seller's allowed chain/token choices, and re-points an existing pending intent when the buyer changes rail. When `ORACLE_QUOTING_ENABLED=true`, the backend ignores client-supplied `amount`, loads the seller offer price, computes a depeg-protected quote, and uses the computed settlement amount for the provider intent.
**Auth required:** Bearer JWT (buyer)
**Request body additions:** `provider?: "request.network" | "amn.scanner"`, `token`, `network`, `metadata.templateId?`.
**Response 200:** `{ success: true, data, quote? }`. `quote` includes `settleAmount`, `token`, `tokenPriceUSD`, `depegAdjustmentBps`, `roundingBps`, and `expiresAt` when oracle quoting is enabled.
**Errors:** `400` for unsupported/disallowed chain-token choice, `422 DEPEG_LIMIT_EXCEEDED` when the settlement token exceeds the depeg hard cap, `503 ORACLE_UNAVAILABLE` when rates are stale or unavailable.

### GET /api/payment/request-network/permit-availability

**Description:** Checks whether the backend relayer can sponsor an EIP-2612 `permit()` transaction for a chain/token. This is partial gasless support: it removes the approval transaction gas only; the buyer still sends the final payment transaction.
**Auth required:** Bearer JWT
**Query params:** `chainId`, `token`
**Response 200:** `{ success: true, data: { available, reason?, relayer?, balanceWei?, requiredWei? } }`

### POST /api/payment/request-network/:paymentId/permit

**Description:** Broadcasts a buyer-signed EIP-2612 permit through the backend relayer. The route validates the permit against the payment's actual in-house checkout block so the relayer only sponsors real pending payments and the expected fee-proxy spender.
**Auth required:** Bearer JWT (buyer who owns the payment)
**Request body:** `{ owner, spender, value, deadline, v, r, s }`
**Response 200:** `{ success: true, data: { txHash, allowance } }`
**Limitations:** Only permit-capable tokens/chains qualify. Mainnet USDT is not permit-capable; full gasless payment still requires a forwarder or account-abstraction/paymaster design.

### GET /api/payment/request-network/:paymentId/checkout

**Description:** Rehydrates the in-house checkout payload for an existing Request Network payment so the frontend can build the on-chain approval/payment transaction without relying on the hosted RN page.
**Auth required:** Bearer JWT (buyer who owns the payment)

### POST /api/payment/request-network/webhook

**Description:** Request Network posts settlement updates here. The route verifies `x-request-network-signature` over the raw body, deduplicates delivery IDs, evaluates the Transaction Safety Provider, and coordinates the payment/ledger update.
**Auth required:** No (signature-protected)
**Response:** `200` when processed or duplicate; `202` when accepted but safety checks are pending; `401` for invalid signature.
**Side effects:** For confirmed/completed events, `blockchain.confirmations` is stored as the accepted confirmation count capped at the effective per-chain threshold before the payment/ledger update is emitted.

> [!note] RN payout/release/refund routes
> `POST /api/payment/request-network/:paymentId/payout/initiate`, `POST /api/payment/request-network/:paymentId/payout/confirm`, `POST /api/payment/request-network/:paymentId/release/confirm`, and `POST /api/payment/request-network/:paymentId/refund/confirm` are registered in `requestNetworkRoutes.ts` but are stub-level implementations. They accept the request and return a 200 but do not yet drive the ledger-gated release/refund orchestration. Use `POST /api/payment/:id/release` and `POST /api/payment/:id/refund` for actual escrow releases.

## AMN Pay Scanner - Pay-in

AMN Pay Scanner is a custom in-house blockchain scanner that replaces the hosted Request Network page for payment monitoring. It speaks the same `PaymentProviderAdapter` interface as the RN adapter.

### POST /api/payment/amn-scanner/webhook

**Description:** AMN Pay Scanner posts settlement confirmations here. The route verifies a `webhookSecret`-based HMAC signature, then runs the Transaction Safety Provider and `PaymentCoordinator` pipeline identical to the RN webhook path.
**Auth required:** No (signature-protected via `AMN_SCANNER_WEBHOOK_SECRET`)
**Request body:** `{ intentId, status, txHash?, transactionHash?, chainId?, confirmations?, ... }` — scanner-specific envelope. Current scanner payloads usually use `txHash`; `confirmations` may be omitted once the scanner has already waited for the configured threshold.
**Response:** `200` processed; `401` bad signature; `400` missing `intentId` or unknown format; `404` payment not found.
**Side effects:** Same as the RN webhook — updates [[Payment]], advances [[PurchaseRequest]], accepts/rejects offers, emits socket events when safety checks pass. Backend `2.6.82+` treats scanner `status: "confirmed"` as a settlement status for Transaction Safety Provider evaluation and confirmation persistence; if neither verifier evidence nor payload `confirmations` exists, it stores the effective chain threshold so the dashboard does not show a paid scanner transaction with `0` confirmations. Settled confirmation counts are capped at the accepted threshold instead of continuing to grow.

> [!note] Provider value
> Payments created via the AMN Pay Scanner have `provider: 'amn.scanner'` in the database. This is distinct from `request.network` and `shkeeper`.

### GET /api/admin/scanner/status

**Description:** Proxies to `AMN_SCANNER_URL/scanner/status` and returns the scanner's internal state.
**Auth required:** Bearer JWT (`admin`) — `authenticateToken` + `authorizeRoles('admin')` are now applied (the previously documented security gap — unauthenticated access — has been fixed in commit `1d881c5`).
**Response 200:** Scanner status JSON forwarded from the upstream service.

### POST /api/admin/scanner/webhooks/retry

**Description:** Triggers a manual retry of failed/pending scanner webhooks.
**Auth required:** Bearer JWT (`admin`)
**Request body:** `{ intentId?: string }` — omit to retry all pending.

## Legacy SHKeeper - Pay-in

> [!warning] Historical route family
> The current `app.ts` mounts Request Network routes, not `services/payment/shkeeper/*`. Keep this section only for legacy record migration and old operational context.

### POST /api/payment/shkeeper/intents

**Description:** Creates a SHKeeper pay-in intent. The server provisions an invoice on SHKeeper, stores a [[Payment]] with `provider: "shkeeper"`, `direction: "in"`, returns the hosted-widget URL.
**Auth required:** Bearer JWT (buyer)
**Request body:**
```ts
{
  purchaseRequestId: string;
  sellerOfferId: string;
  amount: number;
  sellerId: string;
  token?: string;     // default "USDT"
  network?: string;   // default "bsc"
  metadata?: Record<string, unknown>;
}
```
**Response 200:**
```json
{
  "success": true,
  "data": {
    "paymentId": "...",
    "paymentUrl": "https://pay.amn.gg/invoice/...",
    "externalId": "AMN_...",
    "expiresAt": "2026-05-23T11:00:00.000Z"
  }
}
```
**Errors:** `400` missing fields, `401` not authenticated, `500` SHKeeper error.
**Side effects:** Emits `payment-created` globally and to the request room.

### POST /api/payment/shkeeper/webhook

**Description:** SHKeeper posts here when an invoice changes state. Handles both raw-string and JSON bodies and verifies the HMAC signature (`x-shkeeper-signature` against the raw body using `SHKEEPER_WEBHOOK_SECRET`).
**Auth required:** No (signature-protected)
**Body:** The SHKeeper callback envelope (`external_id`, `crypto`, `addr`, `fiat`, `balance_fiat`, `balance_crypto`, `paid`, `status`, `transactions[]`).
**Response 200:** `{ success: true }`
**Side effects:**
- Updates the matching [[Payment]] to `completed` (`OVERPAID` and `PAID` both count). Note: `'completed'` is the terminal state for SHKeeper payments but is **not** counted as `successfulPayments` in `GET /api/payment/stats`.
- Releases or rejects [[SellerOffer]] siblings (the chosen offer becomes `accepted`, others `rejected`).
- Updates [[PurchaseRequest]] status to `payment` / `processing`.
- Emits `seller-offer-update` to each affected seller room and `purchase-request-update` to the request room.

> ⚠️ **NOT IMPLEMENTED:** `GET /api/payment/shkeeper/status/:paymentId` does not exist. SHKeeper payment status is delivered via socket events only — there is no HTTP polling endpoint.

### POST /api/payment/shkeeper/confirm-transaction

**Description:** Manual fallback when the webhook misses — the frontend calls this after the buyer signs the EVM transaction directly. Coordinated through `PaymentCoordinator` to avoid double updates.
**Auth required:** Bearer JWT
**Request body:** `{ paymentId, transactionHash, network? }`
**Response 200:** `{ success, message, data: { paymentId, transactionHash, status } }`
**Side effects:** Closes the SHKeeper invoice session, then runs the same offer/request updates as the webhook.

### POST /api/payment/shkeeper/test

**Description:** Smoke-tests the real SHKeeper API. Development only.
**Auth required:** No

### POST /api/payment/shkeeper/callback-test (and GET equivalent)

**Description:** Echo-style endpoints used during webhook configuration.
**Auth required:** No

### POST /api/payment/shkeeper/create-test-payment

**Description:** Inserts a sample [[Payment]] row to exercise the webhook handler.
**Auth required:** No

### POST /api/payment/shkeeper/trigger-webhook

**Description:** Sends a fake webhook payload to the local webhook endpoint for end-to-end testing.
**Auth required:** Bearer JWT

### GET /api/payment/shkeeper/wallet-monitor/status

**Description:** Returns the wallet-monitor state (`isMonitoring`, watched wallet addresses).
**Auth required:** No

### GET /api/payment/shkeeper/auto-webhook/status

**Description:** Returns the auto-webhook fallback monitor state.
**Auth required:** No

### GET /api/payment/shkeeper/webhook-stats

**Description:** Counters for webhook deliveries (success / failure / duplicates).
**Auth required:** Bearer JWT (admin)

## Legacy SHKeeper - Release / Refund (escrow)

These build an admin-signed transaction off-chain and require a follow-up confirm with the broadcast tx hash. Source: `shkeeperService.buildAdminSignedTxPayload` and `confirmAdminTx`.

**⚠️ Path correction:** The `/shkeeper/` segment is NOT present in the actual release/refund routes. The correct paths are under `/api/payment/:id/…` (not `/api/payment/shkeeper/:id/…`).

### POST /api/payment/:id/release

**Description:** Prepares the admin-signed payload to release escrow to the seller. Returns the raw payload — the admin client signs and broadcasts.
**Auth required:** Bearer JWT (admin)
**Response 200:** `{ success: true, data: { /* tx payload */ } }`

### POST /api/payment/:id/release/confirm

**Description:** Records the broadcast transaction hash for the release; marks the payment as released, updates [[PurchaseRequest]] to `seller_paid` and emits `purchase-request-update` (`type: payment_released`).
**Auth required:** Bearer JWT (admin)
**Request body:** `{ txHash: string }`
**Errors:** `400` missing `txHash`.

### POST /api/payment/:id/refund

**Description:** Mirror of release, but returns the escrow to the buyer.
**Auth required:** Bearer JWT (admin)

### POST /api/payment/:id/refund/confirm

**Description:** Records the refund tx hash; emits `purchase-request-update` (`type: payment_refunded`).
**Auth required:** Bearer JWT (admin)
**Request body:** `{ txHash: string }`

## Legacy SHKeeper - Payouts

Historical payouts were SHKeeper-side outbound transfers. Current routine releases should use ledger-gated release/refund orchestration instead.

### POST /api/payment/shkeeper/payout

**Description:** Create a payout task. The server creates a [[Payment]] row with `direction: "out"` and provider task id, then returns the SHKeeper task descriptor.
**Auth required:** Bearer JWT (admin)
**Request body:**
```ts
{
  purchaseRequestId: string;
  sellerOfferId: string;
  buyerId: string;
  sellerId: string;
  amount: number;
  recipientAddress: string;
  token?: string;     // default "USDT"
  network?: string;   // default "bsc"
  metadata?: Record<string, unknown>;
}
```
**Response 200:** `{ success, data: { payoutId, taskId, status }, message }`
**Side effects:** `emitGlobalEvent('payout-created', { ... })`.
**Errors:** `400` for each missing required field, `500` upstream error.

### GET /api/payment/shkeeper/payout/status/:taskId

**Description:** Polls SHKeeper for the current task status.
**Auth required:** Bearer JWT
**Response 200:** `{ success, data: { /* status payload */ } }`

### POST /api/payment/shkeeper/payout/webhook

**Description:** SHKeeper webhook for payout state changes. Handled by `processPayoutWebhookEvent`. Emits `payout-completed` (or `payout-updated`) global socket events on success.
**Auth required:** No (signature checked)
**Response 200/400:** `{ success, message, data }`

## Legacy Web3 Wallet-Direct (DePay)

> ⚠️ **NOT IMPLEMENTED:** `POST /payment/depay/intents` (`createDePayIntent`) does not exist in the codebase.

### POST /api/payment/decentralized/save

**Description:** Persists a Web3-initiated payment record.
**Auth required:** Bearer JWT (enforces `userId` ownership match)
**Request body:**
```ts
{
  purchaseRequestId: string;
  buyerId: string;
  sellerId?: string;
  amount: number;
  currency?: string;
  transactionHash?: string;
  network?: string;
  token?: string;
  walletAddress?: string;
  metadata?: Record<string, unknown>;
}
```

### GET /api/payment/decentralized/status/:paymentId

**Description:** Returns the latest status for a decentralized payment.
**Auth required:** No

### PUT /api/payment/decentralized/update

**Description:** Update a decentralized payment's status / confirmations.
**Auth required:** Bearer JWT (owner or admin)
**Request body:** `{ paymentId, status, confirmations? }`

### GET /api/payment/decentralized/receiver

**Description:** Returns the configured escrow receiver wallet address.
**Auth required:** No

### GET /api/payment/decentralized/history/:userId

**Description:** Decentralized payment history for a user.
**Auth required:** Bearer JWT (self or admin)

### POST /api/payment/decentralized/verify/:paymentId

**Description:** Re-verifies a single decentralized payment against the chain. `paymentId` is a **path parameter** as shown.
**Auth required:** Bearer JWT (owner or admin)

### POST /api/payment/decentralized/verify-all-pending

**Description:** Iterates all `pending` decentralized payments and re-verifies them.
**Auth required:** Bearer JWT (admin only)

### POST /api/payment/decentralized/admin-payout

**Description:** Pay a seller directly from an admin hot wallet. This bypasses the newer ledger-gated release/refund orchestration and should not be used for routine releases.
**Auth required:** Bearer JWT (admin)
**Request body:**
```ts
{
  purchaseRequestId: string;
  receiverWalletAddress: string;
  amount: number;
  currency?: string;     // default "USDT"
  network?: string;      // default "BSC"
}
```
**Response 200:** `{ success, data: { /* payout receipt */ } }`

## Derived Destinations

These endpoints manage per-(buyer, sellerOffer) ephemeral payment addresses.

| Method | Route | Auth | Purpose |
|--------|-------|------|---------|
| `GET` | `/api/payment/derived-destinations` | Admin | List destinations with filters/pagination |
| `POST` | `/api/payment/derived-destinations/sweep` | Admin | Sweep **all** active destinations |
| `POST` | `/api/payment/derived-destinations/:id/sweep` | Admin | Sweep **one** destination |
| `POST` | `/api/payment/derived-destinations/:id/balance` | Admin | Refresh on-chain balance for one destination |
| `GET` | `/api/payment/derived-destinations/config/health` | Admin | Verify xpub and sweep signer config |
| `POST` | `/api/payment/derived-destinations/cron/start` | Admin | Start the sweep cron |
| `POST` | `/api/payment/derived-destinations/cron/stop` | Admin | Stop the sweep cron |
| `GET` | `/api/payment/derived-destinations/cron/status` | Admin | Check if sweep cron is running |

### `GET /api/payment/derived-destinations`

Query params: `buyerId`, `sellerOfferId`, `status` (`active|swept`), `address`, `chainId`, `page`, `limit`.

**Response 200:**
```json
{
  "success": true,
  "data": {
    "destinations": [
      {
        "_id": "...",
        "buyerId": "...",
        "sellerOfferId": "...",
        "address": "0x...",
        "derivationPath": "m/44'/60'/0'/0/5",
        "derivationIndex": 5,
        "chainId": 56,
        "status": "active",
        "balance": "1000000000",
        "sweepCount": 0,
        "totalSwept": "0",
        "createdAt": "..."
      }
    ],
    "pagination": { "page": 1, "limit": 20, "total": 42 }
  }
}
```

### `POST /api/payment/derived-destinations/sweep`

Body: `{ chainId?: number, tokenSymbol?: string, minSweepAmount?: string }` — all optional.

**Response 200:** `{ success: true, data: { results: SweepResult[] } }`

Each `SweepResult`:
```ts
{
  destinationId: string;
  address: string;
  status: 'success' | 'error' | 'skipped';
  txHash?: string;
  amount?: string;
  error?: string;
}
```

### `POST /api/payment/derived-destinations/:id/sweep`

Same result shape as above, but for a single destination.

### `GET /api/payment/derived-destinations/config/health`

**Response 200:**
```json
{
  "success": true,
  "data": {
    "xpubValid": true,
    "xpubFingerprint": "0xabcd...",
    "signerType": "build-only",
    "signerHealthy": true,
    "chainId": 56,
    "masterWallet": "0x..."
  }
}
```

## Frontend PaymentProvider type

`src/types/payment.ts` defines `PaymentProvider` as:

```ts
type PaymentProvider = 'request.network' | 'test' | 'other';
```

> ⚠️ **Type gap (M37):** Despite both SHKeeper and the legacy wallet-direct (DePay/decentralized) flows being active in production, neither `'shkeeper'` nor `'decentralized'` appears in this union. Any frontend code that branches on `provider` will treat both as `'other'` or fall through a switch default. The backend stores the literal strings `"shkeeper"` and `"decentralized"` in the database; the mismatch exists only in the frontend type definition.

## Status model

[[Payment]] uses the statuses below across all providers:

- `pending` - intent created, awaiting on-chain settlement
- `processing` - settlement seen, awaiting confirmations
- `confirmed` - fully credited (intermediate; sometimes skipped). **Note:** this is the only status counted as `successfulPayments` in `GET /api/payment/stats`.
- `completed` - confirmed, escrow funded. Terminal state for SHKeeper and DePay. **Not** counted in `successfulPayments` stats — see stats undercounting note above.
- `failed` - intentionally failed (expired, declined, refused)
- `cancelled` - cancelled by user/admin
- `released` - escrow released to seller through the release/refund orchestration and custody signer
- `refunded` - escrow returned to buyer

Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded`.

## Confirmation thresholds (admin)

### `GET /api/admin/settings/confirmation-thresholds`

**Auth:** Admin only  
**Response 200:**
```json
{
  "success": true,
  "data": [
    { "chainId": 56, "threshold": 200, "source": "default" },
    { "chainId": 1, "threshold": 50, "source": "config" }
  ]
}
```

### `PATCH /api/admin/settings/confirmation-thresholds/:chainId`

**Auth:** Admin only  
**Body:** `{ "threshold": 250 }`
**Description:** Updates the runtime confirmation threshold for a chain. Values below the chain's built-in acceptance floor are clamped to that floor; higher values are allowed. The in-memory cache is invalidated immediately so the next `TransactionSafetyProvider` evaluation uses the effective value.
**Response 200:**
```json
{
  "success": true,
  "message": "Confirmation threshold for chain 56 updated to 250",
  "data": { "chainId": 56, "threshold": 250 }
}
```

### `GET /api/admin/settings/confirmation-thresholds/history`

**Auth:** Admin only  
**Description:** Returns paginated audit log of past confirmation threshold changes. Each entry records the admin who made the change, old/new threshold values, chain ID, and timestamp. Backed by the `ConfigSettingHistory` Mongoose model added in commit `27fb15a` (task #9).  
**Response 200:** `{ success: true, data: [{ chainId, oldThreshold, newThreshold, changedBy, changedAt }] }`

> **Note:** This endpoint was previously documented as NOT IMPLEMENTED. It was added in commit `27fb15a` and is now live at `/api/admin/settings/confirmation-thresholds/history`.

## Payments awaiting confirmation (admin)

### `GET /api/admin/payments/awaiting-confirmation`

**Auth:** Admin only  
**Query:** `page`, `limit`, `chainId` (optional)  
**Description:** Lists payments that have an on-chain transaction hash but have not yet reached sufficient confirmations (i.e. `metadata.transactionSafety.status === 'pending'` or `escrowState` is not funded/released/refunded).  
**Response 200:**
```json
{
  "success": true,
  "data": [
    {
      "_id": "...",
      "paymentId": "...",
      "status": "pending",
      "amount": { "amount": 12.5, "currency": "USDC" },
      "blockchain": { "network": "bsc", "transactionHash": "0x...", "confirmations": 3 },
      "metadata": { "transactionSafety": { "status": "pending", "checks": [...] } },
      "createdAt": "2026-05-28T..."
    }
  ],
  "pagination": { "page": 1, "limit": 25, "total": 4, "totalPages": 1 }
}
```

## Request Network multichain registry (admin)

### `GET /api/admin/rn/networks`

**Auth:** Admin only  
**Response 200:**
```json
{
  "success": true,
  "data": [
    {
      "chainId": 56,
      "name": "BNB Smart Chain",
      "shortName": "BSC",
      "rpcUrl": "https://bsc-dataseed.binance.org/",
      "publicRpcUrl": "https://bsc-rpc.publicnode.com",
      "blockExplorer": "https://bscscan.com",
      "proxyAddress": "0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9",
      "nativeCurrency": { "name": "BNB", "symbol": "BNB", "decimals": 18 },
      "confirmationThreshold": 200,
      "tokens": [
        { "address": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", "symbol": "USDC", "decimals": 18, "name": "Binance-Peg USD Coin" },
        { "address": "0x55d398326f99059ff775485246999027b3197955", "symbol": "USDT", "decimals": 18, "name": "Binance-Peg BSC-USD" }
      ]
    }
  ],
  "meta": { "chainCount": 5, "tokenCount": 10 }
}
```

### `POST /api/admin/rn/networks/reload`

**Auth:** Admin only
**Description:** Reloads the chain and token registries from disk (`supportedChains.json` and `tokens.json`). Returns `{ success: true, message: 'Registry reloaded from disk' }`. Use this after updating the JSON files without restarting the server.

> **Note:** This route is now implemented (commit `5681abf`). Earlier docs incorrectly listed it as not implemented.

### `POST /api/admin/rn/networks/probe/:chainId`

**Auth:** Admin only
**Description:** Performs a live on-chain probe for the specified chain: verifies RPC reachability, checks for deployed proxy contract bytecode (`eth_getCode`), and test-calls the proxy with a dummy payload to confirm it reverts meaningfully. Returns:
```json
{
  "success": true,
  "data": {
    "chainId": 56,
    "reachable": true,
    "hasCode": true,
    "callValid": true,
    "blockNumber": "0x...",
    "latencyMs": 120
  }
}
```
Errors: `400` if `chainId` is not a number; `404` if the chain is not in the registry; `500` on RPC failure.

> **Note:** This route is now implemented (commit `5681abf`). Earlier docs incorrectly listed it as not implemented.

## Related

- [[Escrow Flow]]
- [[Request Network Integration Constraints]]
- [[Payout Flow]]
- [[Socket Events]]
