# Solana Payments

Peltier lets users pay **from Solana** — native SOL or any SPL token — while
settlement lands as USDC on the EVM destination of your choice, or **on Solana
itself** (`destination_chain: 7565164` with a base58 address). It reuses the same
session table, status lifecycle, SSE stream, and webhooks as the EVM flow: from
the integrator's side a payment from Solana is indistinguishable from a payment
from Arbitrum. The reverse direction — an *EVM* deposit settling to Solana — runs
through the standard EVM flow (see [Supported Chains](/docs/supported-chains.md)).

The deposit address is a program-owned vault whose routing (destination, refund
address, deadline) is **pinned on-chain before the address is returned** — it can
never change afterward, and the same non-custodial guarantees as the EVM side hold
(no server-held keys; the operator is a gas-payer, never a custodian). The payer
signs exactly **one** transaction: fund the vault.

## **POST** `/sessions/solana`

Create an anonymous P2P session paid from Solana — native SOL or any SPL token,
swapped to USDC client-side. The destination is one of the supported EVM chains,
or Solana itself (`destination_chain: 7565164` with a base58 `destination_address`).

### Request Body

```json
{
  "destination_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "destination_chain": 8453,
  "refund_address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
}
```

`refund_address` (base58 Solana, **required**) is pinned on-chain as the only place
a refund can ever go — the relayer can bounce a failed session there immediately,
and after the 24 h deadline the refund becomes permissionless.

### Response 200 OK

```json
{
  "id": "a1b2c3d4-…",
  "status": "pending",
  "deposit_address": "«vault USDC token account»",
  "intent_pda": "«per-session PDA»",
  "program_id": "«PeltierSvm program»",
  "mint": "EPjFW…Dt1v",
  "expires_at": 1714000000,
  "init_tx": "«relayer-paid on-chain setup signature»",
  "init_intent": { /* dest_recipient, dest_domain, refund_to, relayer, deadline, vault, … */ }
}
```

The wallet then signs **one** funding transaction — a client-side Jupiter swap of
any token into the vault (`destinationTokenAccount` = `deposit_address`), or a
direct transfer if it is already USDC.

## **POST** `/sessions/:id/solana-burner`

Attach a Solana deposit address to an existing **checkout** session (created via
`POST /v1/payment_sessions`). Gated by the publishable key + per-session
`client_secret`, exactly like `POST /sessions/:id/register`. The client only picks
the settlement *chain* — the settlement *address* is resolved server-side from the
merchant's pinned [destinations](/docs/merchant-accounts.md).

### Request Body

```json
{
  "dest_chain_id": 8453,
  "refund_address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
}
```

### Response 200 OK

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "pending",
  "deposit_address": "«vault USDC token account»",
  "intent_pda": "«per-session PDA»",
  "init_tx": "«relayer-paid on-chain setup signature»",
  "init_intent": { /* pinned routing (merchant destination, refund_to, …) */ },
  "expires_at": 1714000000
}
```

## **POST** `/sessions/:id/solana-refund`

Return stuck funds from a Solana vault (terminally failed or expired sessions).
**No secret, no request body**: the refund recipient was pinned on-chain at session
creation and cannot be operator-chosen. Only `expired` / `failed` sessions qualify.
Checkout sessions additionally require the pk_ + `client_secret` guard.

### Response 200 OK

```json
{
  "tx_signature": "5K7m…",
  "refund_address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
  "token": "EPjFW…Dt1v",
  "amount": "full"
}
```

## **GET** `/solana/blockhash`

Fresh `confirmed` blockhash — the one chain read a wallet-pay frontend needs to
assemble its own transfer transaction (browsers cannot fetch it directly: the
public mainnet RPC 403s requests carrying an `Origin` header). A blockhash is
public, unforgeable data.

### Response 200 OK

```json
{
  "blockhash": "2r7PiuRF4M3FpdTHywmasgycT6TvgadC6Mq6yxDgYo6D"
}
```

## Automatic bounce

Sessions with a refund target on file get an automatic rescue: when a session fails
terminally, a bounce poller sends the deposit back automatically — on Solana to the
on-chain-pinned `refund_address` — and the session moves to `bounced` (with an SSE
event and a `payment.bounced` webhook for merchant sessions). Sessions without a
`refund_address` stay `failed` and use the manual refund path above.

## Fees & minimums

| Cost | Amount | Notes |
| --- | --- | --- |
| Tx fees + rents | `~0.015 SOL` | Swap tx, ATA rents, message-event account rent, burn tx fee |
| CCTP transfer fee | `0` (standard finality) | Cross-chain settlement to an EVM chain |
| Min native deposit | `~0.06 SOL` | Native SOL sends below this are blocked client-side, before signing (`MIN_NATIVE_SOL`) |
| Platform fee | `0..1%` | Peltier's cut — clamped on-chain to `MAX_FEE_BPS` (1%), applied default 0.8%, split off at settle to the pinned treasury |
| App fee | `0..uncapped` | The app's own cut — `app_fee_bps` committed into the routing hash, paid to the pinned `app_fee_recipient`; bounded only by the `min_out` net floor |

Cross-chain settlement to an EVM chain completes in ~1–2 minutes; settlement on
Solana itself is a single direct transfer and completes in seconds.

## Current limitations

- Deposit detection is polling-based; a Helius/Geyser stream is the planned upgrade
  for scale.
- The vault only ever holds USDC — any non-USDC input is swapped to USDC in the
  payer's own wallet before it reaches the vault — so there is no mixed-asset race.
- EVM sessions registered without a standing refund can only be refunded while the
  client still holds the (EVM) burner key — register one to keep the lost-key escape
  hatch open.
