# Test Mode

Test mode is Peltier's Stripe-parity sandbox: a fully isolated copy of your merchant account that runs entirely on **testnets**, so you can build and exercise the complete payment flow -- checkout, payment links, subscriptions, webhooks -- without moving a cent of real money. Every merchant gets both modes at once; you select a mode simply by which key you use.

## Two modes, two key sets

When you [create a merchant](#merchant-accounts), `POST /v1/merchants` now **dual-mints** -- it returns a live key pair and a test key pair in a single call:

```json
// POST /v1/merchants  { "email": "...", "name": "..." }   no auth
{
  "id": "m_a1b2...",
  "email": "ops@acme.com",
  "name": "Acme Inc",
  "created_at": "2026-06-07T12:00:00Z",
  "keys": {
    "live": { "secret_key": "sk_live_…", "publishable_key": "pk_live_…" },
    "test": { "secret_key": "sk_test_…", "publishable_key": "pk_test_…" }
  },
  "secret_key": "sk_live_…",       // back-compat mirror of keys.live
  "publishable_key": "pk_live_…"   // back-compat mirror of keys.live
}
```

Store all four. The top-level `secret_key` / `publishable_key` still mirror the **live** pair, so existing integrations keep working unchanged -- test mode is purely additive.

## Mode is in the prefix

Keys now encode their mode in the prefix -- base prefix + a `live_`/`test_` infix + 24 base62 characters:

| Prefix | Mode | Runs on |
| --- | --- | --- |
| `sk_live_…` / `pk_live_…` | Live | Mainnet chains, real money. |
| `sk_test_…` / `pk_test_…` | Test | Testnet chains, no real money. |

You never pass a mode flag -- the key string *is* the mode. A `sk_test_`/`pk_test_` key is test; a `sk_live_`/`pk_live_` key is live. `whsec_` webhook signing secrets are **unchanged** (no mode in the prefix). When you mint additional keys, `POST /v1/api_keys` echoes the resolved mode back as `"mode": "live" | "test"`.

## Isolation

The two modes are fully isolated server-side. A test key can only see and create **test** resources (sessions, payment links, subscriptions, destinations, webhook endpoints, and api-keys); a live key only **live** ones. A test session may only touch testnet chains, a live session only mainnet -- enforced by the backend. Nothing crosses over and there is nothing to clean up: throw-away test data never mingles with production.

## Testnet chains

A test key routes to the testnet counterpart of each supported chain. Deposit in testnet USDC (or testnet gas) and settlement lands as testnet USDC:

| Chain | Testnet | Chain ID |
| --- | --- | --- |
| Ethereum | Sepolia | `11155111` |
| Optimism | Sepolia | `11155420` |
| Polygon | Amoy | `80002` |
| Base | Sepolia | `84532` |
| Arbitrum | Sepolia | `421614` |
| Monad | testnet | `10143` |
| HyperEVM (destination only) | testnet | `998` |

Solana test settlement runs on **devnet**, configured via the `SOLANA_RPC_URL` env var rather than a separate chain id. The live chain ids (`1`, `10`, `137`, `8453`, `42161`, `143`, `999`) are unchanged -- see [Supported Chains](#supported-chains). Hand the widget a `pk_test_` key and it offers these testnet chains automatically (a `pk_live_` key offers mainnet).

Fund a test wallet with free testnet USDC and gas from **[faucet.circle.com](https://faucet.circle.com)** -- Circle's faucet mints testnet USDC on every EVM testnet above.

## Quick Start (no real funds)

The primary path -- **same-chain testnet USDC in, testnet USDC out** -- works out of the box with no bridge or swap venue. End to end, using Base Sepolia (`84532`):

1. **Create a merchant** and grab the test pair.

```bash
curl -X POST https://api.peltier.xyz/v1/merchants \
  -H 'Content-Type: application/json' \
  -d '{ "email": "ops@acme.com", "name": "Acme Inc" }'
# → keys.test.secret_key = sk_test_…, keys.test.publishable_key = pk_test_…
```

2. **Pin a testnet destination** with your `sk_test_` key:

```bash
curl -X PUT https://api.peltier.xyz/v1/destinations/84532 \
  -H 'Authorization: Bearer sk_test_…' \
  -H 'Content-Type: application/json' \
  -d '{ "address": "0xYourTestnetDestination..." }'
```

3. **Create a test checkout session** with the same `sk_test_` key (see [Checkout Flow](#checkout-flow)):

```bash
curl -X POST https://api.peltier.xyz/v1/payment_sessions \
  -H 'Authorization: Bearer sk_test_…' \
  -H 'Content-Type: application/json' \
  -d '{ "amount": "10000000" }'
# → { "session_id": "...", "client_secret": "cs_…", "expires_at": ... }
```

4. **Embed the widget** with the `pk_test_` publishable key. Because it is a test key, the widget offers the testnet chains:

```tsx
import { PeltierProvider, PeltierCheckout } from '@peltier/react'

<PeltierProvider
  apiUrl="https://api.peltier.xyz"
  publishableKey="pk_test_…"    // pk_test_ ⇒ testnet
>
  <PeltierCheckout
    sessionId="a1b2c3d4-..."
    clientSecret="cs_…"
    destinationAddress="0xYourTestnetDestination..."
    destinationChainId={84532}
    onComplete={(sessionId) => console.log('paid (test)', sessionId)}
  />
</PeltierProvider>
```

5. **Pay with testnet USDC** from a wallet funded at [faucet.circle.com](https://faucet.circle.com), on the same testnet as your destination (Base Sepolia here).

6. **Observe test webhooks.** Deliveries carry `"livemode": false` and `"mode": "test"`:

```json
{
  "session_id": "a1b2c3d4-...",
  "event_type": "payment.succeeded",
  "amount": "10000000",
  "settled_amount": "10000000",
  "livemode": false,
  "mode": "test"
}
```

`payment.*` and `subscription.*` payloads both carry `livemode` / `mode` (Stripe parity: `livemode:false` in test). Each merchant has **one webhook endpoint per mode**, and a test delivery is signed with the **test** endpoint's `whsec_` secret -- so configure a test endpoint (`PUT /v1/webhook_endpoints` with your `sk_test_` key) and verify against that secret. See [Webhooks](#webhooks) for the signature contract. The public link resolve `GET /links/{slug}` likewise reports `"mode"`, so a hosted [payment link](#payment-links) page knows which network to offer.

## Limitation: cross-token / cross-chain on testnet

Same-chain testnet USDC-in settlement -- the Quick Start above -- needs no external venue and works today. **Cross-token** (paying a non-USDC token) and **cross-chain** (depositing on one testnet, settling on another) additionally need per-chain venue wiring that ships empty for testnets:

- **Swap / bridge venues.** Fill the testnet swap-router and Across venue addresses in `backend/src/chains.rs` -- these are populated for mainnet but empty for the testnets.
- **Testnet RPCs.** Point each testnet at an RPC via `RPC_URL_<testnetId>` (e.g. `RPC_URL_84532` for Base Sepolia).

Until those are set, stick to same-chain testnet USDC deposits -- which is all the Quick Start requires.
