Documentation Index
Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt
Use this file to discover all available pages before exploring further.
Crypto refunds — operator runbook
This runbook is for platform operators (super-admins) processing NOWPayments crypto refunds from the admin dashboard. The tenant-facing view of the same flow lives in Cryptocurrency payments. The refund button is exposed inline in the admin transactions table on any row whosepayment_method is crypto_nowpayments. There is no
tenant-side refund button — crypto refunds are operator-only at launch
to prevent self-service mistakes that would be irrecoverable.
When to refund
Process a refund when ALL of the following hold:- The tenant has opened a support ticket with a clear refund reason (wrong-amount transfer, dispute closed in tenant’s favour, accidental top-up, etc.).
- The tenant has provided a destination wallet address. Confirm with the tenant via the same channel as the ticket — never accept an address from a different channel without re-confirmation.
- The destination address is on the same chain as the original payment. Cross-chain refunds will lose the funds; refuse them.
- The tenant is in the same jurisdiction window the operator is comfortable refunding (consult legal / finance for edge cases).
- The original top-up is recent enough that the refund makes sense. Operationally we cap at 90 days from the original transaction — older requests need escalation.
When to refuse
Refuse (or escalate) when ANY of these hold:- The original transaction is older than 90 days.
- There is an active fraud review or chargeback dispute on the org.
- The destination wallet address is suspected of fraud or sanctions exposure (consult the AML SOP).
- The amount requested exceeds the original top-up.
- The tenant insists on a different chain than the original payment.
- The tenant’s spending pattern shows the refunded amount has already been spent on services rendered.
Step-by-step
-
Verify the ticket. Open the tenant’s support ticket. Confirm the
original transaction id (
txn_*), the requested refund amount, and the destination wallet address. - Confirm wallet address out-of-band. Reply on the ticket asking the tenant to confirm the destination address. Wait for the tenant to reply with the same address before proceeding — this is the primary defence against ticket compromise.
-
Open admin transactions view. Dashboard → Billing → find the row
for the original transaction. The row’s payment method column shows
crypto_nowpaymentsand the inline action shows a red “Refund crypto” button. -
Click “Refund crypto”. The destructive AlertDialog opens with:
- Destination wallet address — paste the value the tenant confirmed.
- Amount — defaults to the full original amount, max-clamped. Set to the partial amount the tenant requested if applicable.
- Reason — paste the ticket reference (e.g.
ZD-12345 — partial refund for wrong-amount transfer).
-
Review the warning banner. Before the “Submit refund” button
activates, the dialog shows a warning summary:
Refunding $X.XX to wallet abc…def on USDTTRC20. This is irreversible once the network confirms.Verify the truncated address matches the FIRST six and LAST six characters of what the tenant gave you. -
Submit. Click “Submit refund”. The system:
- Records a pending refund row in
credit_transactions(negative amount, statuscrypto_refund_pending). - Calls NOWPayments
POST /v1/payoutsynchronously. - Writes an audit-log entry (
admin.crypto_refund_initiated) with the operator user id, ticket reference, address, and amount.
- Records a pending refund row in
-
Wait for the IPN. NOWPayments fires a
refundedIPN when the on-chain payout confirms. The IPN handler:- Deducts the refund amount from the tenant’s wallet balance.
- Updates the
crypto_topup_intentsrow torefunded. - Emits a second audit-log entry (
billing.crypto_refunded).
- Reply to the ticket. Once the IPN lands and the wallet has been deducted, reply to the ticket with the payout id + on-chain transaction hash (when NOWPayments includes it on the IPN payload).
Reconciliation
To find all crypto refunds processed by the operator team:refunded IPN:
Edge cases
Refund larger than the org’s current wallet balance. The IPN handler’sdeductBalance call is allowed to take the wallet below zero
in this case — the org goes into negative balance pending operator
reconciliation. Surface this as a follow-up task in the same ticket
(“your balance is now -$X — please top up or contact support before
sending again”).
Partial refunds. Fully supported. The amount input is clamped to
the original top-up. Multiple partial refunds against the same original
are allowed; each one is keyed on
(transaction_id, destination_address, amount_cents) so re-submitting
the same partial refund collapses on the deterministic ledger id.
Multi-currency refunds. Out of scope at launch. NOWPayments
payouts are always in the SAME crypto currency as the original
top-up. If the tenant asks for a refund in a different coin, refuse
and offer either (a) refund in the original coin or (b) cancel the
refund and credit the wallet manually in USD.
Refund button is missing on a row. The button only renders when
transaction.payment_method === "crypto_nowpayments". Rows from before
the payment_method backfill (migration 228) may have this column as
NULL. If a crypto row genuinely needs a refund and the button is
missing, escalate to platform engineering — there is a manual SQL path
documented in the internal runbook.
NOWPayments returns a 4xx on the payout call. The button surfaces
the provider’s error message verbatim. The two common ones:
- 422 “Invalid address” — the destination address is not valid for the pay currency. Re-ask the tenant; this typically means a wrong-chain paste.
- 422 “Amount below minimum” — NOWPayments has per-currency minimum payouts. For tiny refunds the only path is to credit the wallet in USD via the bonus credits tool and refuse the on-chain refund.
Audit trail
Every refund initiates two audit-log entries (chain-linked via the tamper-evident audit chain):admin.crypto_refund_initiated— written by the admin controller the moment the operator clicks “Submit refund”. Contains the operator user id, source IP, ticket reason, destination address, amount, original transaction id, payout id from NOWPayments, and initial payout status.billing.crypto_refunded— written by the IPN handler when therefundedIPN lands. Contains the on-chain transaction hash (when available), the actual wallet deduction amount, and the new wallet balance.
resource_id = '<pending_ledger_id>'. Both entries link to the
same credit_transactions.id.