# Order Lifecycle & Refunds

What has to be true for a payment to become orders, the states an order moves through, how a
multisend aggregates them, and when funds come back.

## Payment rules

The backend indexes your payment (the `Paid` event on EVM, the treasury transfer on Solana / TON),
loads the quote named by its `quoteId`, and checks the payment against it. **All** of these must
hold:

| Rule | Detail |
| --- | --- |
| **Same payer** | Sent by the wallet you quoted as `sender`. On Solana that wallet is the fee payer (first signer). |
| **Same token** | The `fromToken` you quoted: the native coin, the ERC-20, or the exact SPL mint. |
| **Exact amount** | The pay target receives **exactly `fromTotal`** base units — not more, not less. |
| **Before the deadline** | The transaction's block time is at or before `deadline`. On EVM the contract reverts a late call; on Solana / TON nothing stops a late transfer — it lands and becomes a refund case. |
| **One `quoteId`** | EVM: the `quoteId` argument. Solana: exactly one memo carrying the id, and only one asset credited to the treasury in that transaction. TON: the text comment. |
| **Not a self-transfer** | Solana / TON: a transfer from the treasury to itself is ignored. |
| **Once** | Each quote is honoured once. |

When a rule is broken:

| What went wrong | What happens |
| --- | --- |
| Different payer, token or amount | **Held for review.** No order, no automatic refund; the payment is flagged to the operator. Contact support with the transaction hash — see [Errors](https://docs.drip.tools/reference/errors.md#payments-held-for-review). |
| Landed after `deadline` | EVM: the contract reverts, nothing is paid. Solana / TON: no order; a refund is recorded with reason `expired`. |
| Unknown `quoteId`, or no readable memo / comment | No order; a refund is recorded with reason `unknown_quote` (an id the backend never issued) or `unattributed` (no readable id at all — Solana / TON). |
| Same `quoteId` paid twice | The first payment is honoured; the second is **held for review** — which payment to return is the operator's call, nothing is refunded automatically. |

## Order states

Once the payment passes the checks, the quote is expanded into **one order per recipient**:

| Status | Meaning |
| --- | --- |
| `pending` | Reserved — the current flow never produces it; an order appears directly as `processing`. |
| `processing` | The testnet payout is queued or in flight. |
| `completed` | The recipient received their tokens. `to.txHash` is the payout transaction. |
| `failed` | The payout could not be delivered to that recipient. |

```
payment indexed → checked against the quote → order(s) → pending / processing → completed
                                                                            ↘ failed → refund
```

You observe these on each [order object](https://docs.drip.tools/api/orders.md#order-object) via `status`, and the payout
transaction via `to.txHash` once `completed`. Payouts usually land within seconds of the payment
being indexed.

## Multisend: aggregate status

A payment with many recipients produces many orders. The [batches view](https://docs.drip.tools/api/orders.md#get-api-orders)
and the [order-by-tx lookup](https://docs.drip.tools/api/orders.md#by-payment-from-tx-hash-ref) collapse them into one
aggregate `status`:

| Aggregate | When |
| --- | --- |
| `processing` | Any recipient is still pending/processing. |
| `completed` | Every recipient was delivered. |
| `failed` | Every recipient failed. |
| `partial` | Some recipients were delivered and some failed. |

For a fully-failed batch the whole payment is refunded on the FROM chain. For a partial batch the
**failed slices are refunded**, capped so that the delivered part still cost at least **$1** at the
quote's locked price (otherwise the multisend minimum could be bypassed). A refund the cap would
reduce to zero is not sent automatically — it is held for the operator.

## Refunds

A refund returns FROM funds to the payer when a payment can't be (fully) honoured:

| Reason | Cause |
| --- | --- |
| `failed_order` | The failed recipients of a settled batch — their FROM slice is returned (capped for a partial batch, see above). |
| `unknown_quote` | The payment referenced a `quoteId` the backend never issued. |
| `unattributed` | A transfer to the treasury with no readable `quoteId` at all (Solana / TON), above a small dust threshold — smaller amounts are ignored. |
| `ambiguous_memo` | One Solana transaction carrying several different `quoteId`s — it cannot be split between them. |
| `expired` | The payment landed after the quote's `deadline` (Solana / TON). |
| `bad_signature` | The stored quote's signature did not verify against a trusted signer key — an operator-side issue, not something a payer can cause. |

Refund states are `refund_pending` (recorded, return not yet sent) and `refunded` (return tx sent).
Refunds are issued by the operator from the receiving account. The record for a refused payment
(`expired`, `unknown_quote`, `unattributed`, `ambiguous_memo`, `bad_signature`) is written when the
payment is indexed; a `failed_order` record appears within a few minutes of the batch settling. The
return transaction follows once it is sent. Track them via the `refund` field on
[`GET /api/order?from_tx_hash=`](https://docs.drip.tools/api/orders.md#by-payment-from-tx-hash-ref) (EVM tx hash or Solana
signature), or in the payer's history ([`GET /api/orders?view=batches`](https://docs.drip.tools/api/orders.md#get-api-orders),
`status=refund` for refunds only). On TON use the history: a refused payment never consumed its
quote, so `from_tx_hash=<quoteId>` does not find it:

**Response**
```json
{
  "data": [ /* … the delivered/failed orders … */ ],
  "refund": {
    "status": "refunded",
    "reason": "failed_order",
    "from": { "symbol": "ETH", "amount": "0.0003", "address": "0xpayer", "txHash": "0x<payment>" },
    "refundTxHash": "0x<return tx>",
    "refundedAt": 1784120300
  }
}
```

> **Tip: Avoid refunds**
>
> The avoidable reasons are `expired` (submit your payment well before the quote `deadline`),
> `unattributed` and `ambiguous_memo` (always send exactly one `quoteId` in the memo / comment).
> Pay the exact `fromTotal`, from the quoted wallet, before the deadline, and never retry a payment
> whose transaction already confirmed — track it instead.

## Tracking to completion

A robust client polls [`GET /api/order?from_tx_hash=`](https://docs.drip.tools/api/orders.md#by-payment-from-tx-hash-ref)
until every order is terminal:

**JavaScript**
```js
const terminal = o => o.status === 'completed' || o.status === 'failed'

for (;;) {
  const { data, refund } = await fetch(`${API}/order?from_tx_hash=${ref}`).then(r => r.json())
  if (data.length && data.every(terminal)) {
    const ok     = data.filter(o => o.status === 'completed').length
    const failed = data.filter(o => o.status === 'failed').length
    console.log(`${ok} delivered, ${failed} failed`)
    if (refund) console.log(`refund ${refund.status}: ${refund.from.amount} ${refund.from.symbol}`)
    break
  }
  await new Promise(r => setTimeout(r, 5000))
}
```

**Python**
```python
terminal = lambda o: o["status"] in ("completed", "failed")

while True:
    r = requests.get(f"{API}/order", params={"from_tx_hash": ref}, timeout=30).json()
    data, refund = r["data"], r["refund"]
    if data and all(map(terminal, data)):
        ok = sum(o["status"] == "completed" for o in data)
        print(f"{ok} delivered, {len(data) - ok} failed")
        if refund:
            print(f"refund {refund['status']}: {refund['from']['amount']} {refund['from']['symbol']}")
        break
    time.sleep(5)
```

`ref` is the EVM tx hash, the Solana signature, or the `quoteId` on TON. For a wait estimate
while orders are still `processing`, call
[`GET /api/orders/load?ref=<ref>`](https://docs.drip.tools/api/orders.md#get-api-orders-load) with the tx hash, the
signature or an order id — on TON pass `data[0].id` from the lookup above, `/orders/load` does
not resolve a `quoteId`; to be notified instead of polling, subscribe to [`GET /api/orders/stream`](https://docs.drip.tools/api/orders.md#get-api-orders-stream).
