Refunds
The short version: refunds work for payments that went through the escrow flow (Coinbase Commerce Payments authorize/capture). They do not work automatically for payments that settled directly via x402 (agent calls) — the funds moved straight payer-to-merchant on-chain and Coal has no recall power. Those are manual-only.
This page covers both paths.
What's refundable automatically
A payment is refundable through Coal's console + API when:
- The session was created via Coinbase Commerce Payments with an
authorize → captureflow. Coal callsauthorize()to lock the payer's USDC in an escrow contract (0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cffon Base mainnet). When the merchant captures, funds release to the merchant payout. If the merchant later refunds, Coal — as the contract's operator — callsrefund()on the same contract to return the funds. - The session is in status
confirmedorpartially_refunded. - The session has an
authId(the escrow contract's reference for the locked payment).
For these, full and partial refunds both work:
1curl -X POST https://api.usecoal.xyz/api/console/payments/refund \2 -H "Authorization: Bearer <session>" \3 -H "Content-Type: application/json" \4 -d '{ "sessionId": "ckt_xxx", "amount": "5.00", "reason": "Customer requested" }'
amount is optional — omit for a full refund of the remaining balance. The endpoint enforces that total refunded ≤ original amount, and tracks each partial refund as its own Refund row with its own on-chain tx.
The on-chain tx returns USDC to the original payer address (whichever wallet signed the original payment).
What's NOT refundable automatically
Two payment shapes settle directly payer-to-merchant without an escrow step. For these, Coal cannot reverse the transfer because Coal never had custody:
- x402 / agent settles (
/api/agent/paywalls/[id]/settleand the bundle proxy at/api/p/{slug}/{path}). The payer signs an EIP-3009transferWithAuthorization; Coal's operator submits it on-chain; USDC moves straight from payer to the merchant's payout (or to the merchant's 0xSplits Push Split contract). After submission, the funds are the merchant's. - Direct
charge()calls through Commerce Payments (single-step, no authorize). Same shape: funds move payer-to-merchant in one step, Coal has nothing to refund.
These payments have no session.authId on the corresponding CheckoutSession row (or no CheckoutSession at all in the agent-settle case). The refund endpoint will reject with INVALID_OPERATION: No payment ID found for refund.
How to refund manually
The merchant has three options for refunds that Coal can't process automatically:
1. Send USDC back from the payout wallet.
1cast send 0x036CbD53842c5426634e7929541eC2318f3dCF7e \2 'transfer(address,uint256)(bool)' \3 <payer-address> <amount-in-base-units> \4 --private-key $MERCHANT_PAYOUT_KEY \5 --rpc-url https://mainnet.base.org
The payer's address is on the receipt page (/verify/{checkoutId}) or in your transactions list. Amount-in-base-units = USD × 1,000,000 (USDC has 6 decimals).
2. Issue store credit instead. If the merchant relationship has an account abstraction (login, balance), credit the user there rather than pushing USDC back. Most agent-pay use cases prefer this — agents care about access, not a wallet refund.
3. For fee-tier merchants: the 5% Coal fee on x402 settles is also not refundable (it landed in Coal's fee recipient address). If you need a refund inclusive of the platform fee, contact us at emmanuel@schemalabs.xyz and we'll send a manual USDC reimbursement for the fee portion.
Why agent payments aren't automatically refundable
x402's gasless UX comes from EIP-3009: the payer signs an authorization, the operator submits it. Once submitted, the transfer is a normal on-chain ERC-20 movement — there's no escrow, no two-phase commit, no reversal mechanism native to the standard. To make agent payments refundable Coal-side, two options exist:
- Wrap every x402 settle in an escrow contract. Coal would receive payer USDC into a contract it controls, hold for N minutes, then forward to merchant. This adds latency, gas, and a custody risk surface. Most agent commerce is genuinely final (per-call API access; can't un-serve a response) — paying that cost across the board to enable refunds on the small minority of cases that need them isn't worth it.
- Per-merchant opt-in deferred capture. A merchant could mark a paywall as "delayed settle" — agent's signed auth is held off-chain by Coal for ~5 minutes before submission, giving a refund window. We have not built this. If it matters for your use case, file an issue at https://github.com/emmanuel39hanks/coal/issues.
Refund tx + webhook
Successful automatic refunds:
- Create a
Refundrow in Postgres (refundtable) - Update the parent CheckoutSession to
refunded(full) orpartially_refunded(partial) - Fire a
payment.refundedwebhook to the merchant URL (signed, withIdempotency-Key: <refund-id>) - Emit a
payment.refundedDA event on 0G
The refund tx hash is the second on-chain reference for the payment. The /verify/{checkoutId} page renders it next to the original capture.
Disputes + chargebacks
There is no chargeback layer for USDC. The chain is final, the merchant's payout is the merchant's. If the merchant refuses to manually refund a justified dispute and the payment didn't go through escrow, the payer has no on-chain recourse via Coal.
This is the trade-off of non-custodial: low fees, no platform middleman, and no central party that can claw back a payment after the fact. If you need chargebacks, use a card processor.
