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. |
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 → refundYou observe these on each 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 and the order-by-tx lookup 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 quoteIds — 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= (EVM tx hash or Solana signature), or in the payer's history (GET /api/orders?view=batches, 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:
{
"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
}
}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= until every order is terminal:
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))
}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> 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.