# Non-Fixed Rate Off-Ramp

Use this flow when a partner converts user crypto deposits into fiat without locking the exchange rate in advance. The final rate and final fiat amount are set when the deposit is confirmed and Spike executes the trade.

In this mode, the **reusable deposit address** is the central integration object. The partner creates an address for a user, source crypto, target fiat, and network. Each compliant deposit to that address creates a separate exchange. There is no expiry window for the address, and partners do not create off-ramp exchanges directly with `POST /v1/exchanges` in this mode.

## When to use this flow

Choose non-fixed-rate off-ramp when:

- The partner wants the simplest off-ramp integration shape — a single deposit address that the user can fund repeatedly without re-quoting.
- The partner's product can present an estimate up front and the final amount after execution.
- The end user is sending crypto from an external wallet on a Spike-supported network.

If the product needs to lock a rate at the time of quote, use the fixed-rate off-ramp flow instead (documented separately).

## Scope

This page describes the reusable-address off-ramp pattern:

- The partner owns the end-user relationship, fiat account selection, customer messaging, and fiat credit or payout experience.
- Spike allocates reusable crypto deposit addresses from its address pool.
- The user sends crypto on the selected network to the address shown by the partner.
- Spike detects the deposit, waits for the required confirmations, creates an exchange automatically, sells the deposited crypto at execution-time market rate, and reports final fiat amounts.
- If funds cannot be converted, Spike follows the refund flow using the refund address supplied by the partner.

Use the [API Reference](../../../api.html) for exact request and response schemas. This page explains the business flow and integration decisions.

For lifecycle, events, idempotency, addresses, refunds, and failure handling, see:

- [Exchange Lifecycle](../concepts/exchange-lifecycle.md)
- [Events and Webhooks](../concepts/events-and-webhooks.md)
- [Idempotency and Retries](../concepts/idempotency.md)
- [Addresses](../concepts/addresses.md)
- [Refunds](../concepts/refunds.md)
- [Failure Handling](../operations/failure-handling.md)

## End-to-end sequence

```mermaid
sequenceDiagram
    autonumber
    participant U as End User
    participant P as Partner
    participant S as Spike
    participant C as Blockchain

    U->>P: Wants to sell crypto
    P->>S: GET /v1/conversion-routes
    S-->>P: Route enabled
    P->>S: POST /v1/deposit-addresses (with refundAddress)
    S-->>P: 201 (depositAddressId, depositAddress)
    P->>S: GET /v1/quotes (optional, indicative)
    S-->>P: Estimate
    P->>U: Show address + asset + network
    U->>C: Send crypto on selected network
    C-->>S: Deposit detected
    S-->>P: deposit.initiated
    Note over C,S: Confirmations accrue
    S-->>P: deposit.progressed (1..N times)
    S-->>P: deposit.received
    Note over S: Trade executes at market
    S-->>P: exchange.completed
    S-->>P: offramp.completed
    P->>U: Credit fiat
```

## Happy path

### 1. Confirm the route is available

`GET /v1/conversion-routes?fromCurrency=<CRYPTO>&toCurrency=<FIAT>&type=OFF_RAMP&enabledOnly=true` to check that the partner can execute the requested direction. For example, a BTC to USD off-ramp is queried as `fromCurrency=BTC&toCurrency=USD&type=OFF_RAMP`.

### 2. Create or reuse a deposit address

Call `POST /v1/deposit-addresses` with the source crypto, target fiat, partner user identifier, network, and preferably a refund address for the same network.

```json
{
  "fromCurrency": "BTC",
  "toCurrency": "USD",
  "externalUserId": "partner-user-456",
  "network": "bitcoin",
  "refundAddress": {
    "address": "bc1qrefundaddressexample000000000000000000",
    "network": "bitcoin"
  }
}
```

The response returns `depositAddressId` and `depositAddress`. Store both. Use a UUID `Idempotency-Key` so a network retry does not allocate a duplicate address. A repeat call with the same `(externalUserId, fromCurrency, toCurrency, network)` while an active address exists returns the existing address. See [Idempotency](../concepts/idempotency.md).

> Setting `refundAddress` here gives the partner a stored default to copy into any future `POST /v1/exchanges/refund` request body — Spike requires `refundAddress` on every refund request and does not fall back to the deposit-address default at refund time. Storing the default at creation is partner-side ergonomics, not a server-side fallback. See [Refunds](../concepts/refunds.md).

### 3. Show an indicative estimate (optional)

If the user enters an intended deposit amount, call `GET /v1/quotes` with `fromCurrency`, `toCurrency`, and `fromAmount`. The response is suitable for display only. The final fiat amount is determined after the deposit is confirmed and the trade executes.

### 4. Display deposit instructions

Show the deposit address, asset, and network clearly. The user must send the selected asset on the selected network. A reusable address can receive multiple deposits over time, and each deposit creates an independent exchange with its own `exchangeId`.

### 5. Track deposit detection and confirmation

Spike detects incoming deposits and links them to the reusable address. Subscribe to deposit webhooks:

- `deposit.initiated` when a deposit is first detected.
- `deposit.progressed` for confirmation-count progress while N < required confirmations.
- `deposit.received` when the required confirmations are reached and the deposit is ready for conversion.

Use `GET /v1/exchanges/by-id` as the authoritative state source once an `exchangeId` is known.

### 6. Track conversion

Once the deposit reaches the required confirmations, the exchange enters `PROCESSING` and Spike sells the deposited crypto at the execution-time market rate. There is no partner authorization step on off-ramp in this mode — Spike executes the trade automatically. When conversion succeeds, Spike emits `exchange.completed` (trading-leg signal) followed by `offramp.completed` (aggregate terminal event) with final amounts and rate.

If any phase fails — deposit rejected, trade rejected, fiat-withdrawal failure — the exchange transitions to `FAILED` and `offramp.failed` fires with a `failureContext` payload (`failureReason`, `failedPhase`). For failures that arrive with funds already received, `refund.required` follows; the partner triggers the refund via `POST /v1/exchanges/refund`. See [Refunds](../concepts/refunds.md) and [Failure handling](../operations/failure-handling.md).

### 7. Credit fiat and reconcile

Credit or settle fiat to the user according to the partner's payment-account setup. Reconcile using `exchangeId`, `depositAddressId`, `externalUserId`, the blockchain transaction hash on `cryptoDeposit`, final `amounts`, and any payout data on the exchange response. See [Reconciliation](../operations/reconciliation.md).

## Decision points

Two decisions to make explicitly:

1. **Where to set `refundAddress`.** At deposit-address creation is strongly recommended. Per-refund overrides are supported via `POST /v1/exchanges/refund` but should be the exception, not the rule.
2. **When to surface fiat to the user.** Do not credit until `exchange.completed` and `amounts.to` are known. The indicative quote is not a guarantee.

## Reusable-address semantics

Behaviors that surprise integrators on first encounter:

- **No expiration in non-fixed-rate mode.** A deposit can arrive any time after the address is shown. There is no "quote expired" failure path.
- **Multiple deposits, multiple exchanges.** Each deposit creates an independent exchange. The partner system must reconcile per-deposit, not per-address.
- **Disablement is one-way for that address.** `DELETE /v1/deposit-addresses/{id}` stops new deposits being processed at that address. After disablement, the partner can create a fresh address for the same user/route.
- **Wrong-network deposits may be unrecoverable.** Make the asset and network unambiguous in the UI. See [Addresses](../concepts/addresses.md).

## What can go wrong

The full negative-scenarios catalog is in [Failure handling](../operations/failure-handling.md). The most common off-ramp paths to be ready for:

- **Quote drift**: the indicative rate is not a price guarantee. Price exposure lasts until the deposit confirms and the trade executes.
- **Wrong asset or network**: deposits on the wrong network may not be recoverable.
- **Below-minimum or dust deposits**: may not be convertible and may not be refundable if network fees would exceed the deposit value.
- **Trade or post-trade failure**: surfaced via `offramp.failed` with `failureContext.failureReason` (e.g. `EXCHANGE_ORDER_CANCELLED`, `EXCHANGE_CREATION_FAILED`, `WITHDRAWAL_FAILED`). Followed by `refund.required` when funds were received; partner triggers the refund.
- **Webhook duplicates and reordering**: process events idempotently, refetch state with `GET /v1/exchanges/by-id`.

## Reconciliation checklist

For every off-ramp deposit, record:

- `depositAddressId` — the address the deposit arrived at.
- `exchangeId` — the exchange created for this specific deposit.
- `cryptoDeposit.transactionHash` — the on-chain proof of the inbound deposit.
- `amounts.from` and `amounts.to` — final crypto-in and fiat-out.
- `amounts.rate` — the executed rate.
- `externalReference.externalUserId` — the partner's user identifier on the deposit address.
- Any refund details from `cryptoRefunds[]` if a refund was issued.

See [Reconciliation](../operations/reconciliation.md) for the full pattern.
