GRCpay refunds funds back to customers in three different situations, each with slightly different semantics. In all of them, the refund side is best-effort: GRCpay tries hard to do the right thing, and when it genuinely can't (sender unknown, dust amount, RPC error) it degrades gracefully rather than parking the wallet in limbo.
Overpayment refunds
When a customer sends more GRC than the wallet's required amount (a typo, a stale fiat→GRC conversion, or an off-by-one somewhere in their checkout) GRCpay detects the overpayment at settlement time and refunds the excess to the sender before forwarding the required amount to the merchant.
The refund goes to the latest contributor, the account whose payment pushed the wallet over the required amount. This is almost always the customer who caused the overpayment in the first place. Their net cost ends up being required + MIN_FEE (they pay for the refund tx themselves, which feels fair: the merchant shouldn't be penalised for their typo). The merchant gets exactly required - MIN_FEE, the same as on a clean payment.
Three corner cases where the refund doesn't happen and the merchant ends up absorbing the overpayment as a tip instead:
Dust overpayment. If the overpayment is smaller than the network fee (e.g. the customer paid 10.0005 GRC for a 10 GRC order), issuing a refund would cost more than it returns. Skipped. The merchant gets the tiny tip.
Sender can't be determined. If GRCpay can't walk the transaction history back to a usable sender address, it can't issue the refund. This is rare but possible with unusual wallet setups. The overpayment is absorbed into the merchant payout.
Refund tx itself fails, persistently. If the refund RPC call throws (wallet locked, network glitch, etc.) GRCpay does not immediately give up and forward the money to the merchant. Instead the wallet stays in funded and the failure is retried with exponential backoff (default intervals 30s, 1m, 2m, 4m) across the next few job-loop cycles. That window gives a real human operator time to actually unlock the daemon (the usual root cause) before we declare the refund hopeless. Only after the retry cap (default 5 attempts) is exhausted does GRCpay fall back to forwarding the full balance so the merchant payout is never blocked indefinitely.
When a refund does go through, the refund txid is recorded on the wallet record in the refundTx field, and the actual GRC amount returned to the customer is in refundAmount. Plugin authors can surface either one to the merchant in their order details screen.
Expired-wallet refunds
If a wallet expires with a non-zero balance (the customer paid late, or the order sat open too long without reaching its target) GRCpay walks the full transaction history and refunds each contributor the amount they originally sent, minus the per-refund network fee. A wallet that received contributions from multiple senders sees multiple refund transactions, one per sender.
Final wallet status depends on what happened:
refunded: at least one refund went out and none failed. The wallet record's tx_out holds the first refund txid; the total GRC returned is in refundAmount; full per-sender details are in the audit log.
norefund: every contribution was below the network fee threshold. Refunding any of them would net-negative the wallet, so nothing is attempted. Terminal state, operator doesn't need to do anything.
error: either no senders could be identified at all, or at least one refund RPC call threw. Any refunds that did succeed are still recorded, but the wallet is parked for an operator to check the audit log and decide what to do with the remainder.
Late-payment refunds
A trickier edge case: what happens when a customer sends GRC to a wallet that has already settled? Picture a shopper who opens a checkout page, walks away for an hour, and eventually clicks “pay” on a stale tab. By that point the order has either been completed by someone else, expired, or been cancelled by the merchant. Without special handling, that GRC would sit silently in GRCpay's hot wallet and the customer would lose it.
GRCpay runs a dedicated late-payment sweep on a separate slow timer (default once an hour, configurable via the LATE_PAYMENT_CHECK_INTERVAL env var; set it to 0 to turn the sweep off entirely). For every terminal-state wallet whose updated_at is within the last 7 days (the LATE_PAYMENT_WINDOW, also env-tunable), it asks the daemon for the current balance, compares it against the last amount GRCpay recorded, and refunds the difference to the latest sender, minus the per-tx fee, same economics as the overpayment flow.
Past the 7-day window the wallet is considered cold: any stale browser session or cached checkout page is long gone, and late deliveries into that address stay in the hot wallet for a human operator to sweep manually. Customers who have genuinely been sitting on a GRCpay address for a month should be reaching out to the merchant through normal channels anyway.
The rules are otherwise identical to overpayment refunds: dust amounts are absorbed into the hot wallet (not worth the fee), sender-not-found cases are logged and left alone, and refund RPC failures retry with the same exponential backoff before giving up. Every successful late refund appears in db_logs under the late_refund action, and the wallet's refundAmount is bumped cumulatively so merchants can always see the total GRC ever returned through that address.