coal
coal

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:

  1. The session was created via Coinbase Commerce Payments with an authorize → capture flow. Coal calls authorize() to lock the payer's USDC in an escrow contract (0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff on Base mainnet). When the merchant captures, funds release to the merchant payout. If the merchant later refunds, Coal — as the contract's operator — calls refund() on the same contract to return the funds.
  2. The session is in status confirmed or partially_refunded.
  3. The session has an authId (the escrow contract's reference for the locked payment).

For these, full and partial refunds both work:

bash
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]/settle and the bundle proxy at /api/p/{slug}/{path}). The payer signs an EIP-3009 transferWithAuthorization; 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.

bash
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:

  1. 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.
  2. 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 Refund row in Postgres (refund table)
  • Update the parent CheckoutSession to refunded (full) or partially_refunded (partial)
  • Fire a payment.refunded webhook to the merchant URL (signed, with Idempotency-Key: <refund-id>)
  • Emit a payment.refunded DA 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.