Payments
Verify payments
The blockchain is the source of truth. A payment completes only when Muny's verification service has inspected the settled transaction — never because a browser, a wallet or a redirect said so.
What is checked#
For every candidate transaction, Muny's server checks every one of these conditions:
| Check | Failure code | Why |
|---|---|---|
| Same Solana network as the payment (devnet vs mainnet) | wrong_network | Test transfers can never pay live payments. |
| Transaction succeeded (no on-chain error) | transaction_failed | A failed transaction moved nothing. |
| Commitment ≥ confirmed | not_confirmed | Unconfirmed transactions can still be dropped. |
| Carries this payment's reference key | missing_reference | Ties the transfer to exactly one payment. |
| Correct token mint | wrong_mint | Another token isn't MUNY. |
| Recipient is the settlement wallet owner | wrong_recipient | The destination is fixed at creation. |
| Recipient's balance grew by exactly the amount | underpaid / overpaid | Computed from pre/post token balances in base units. |
| Signature not already used by another payment | — | Unique constraint: one signature settles at most one payment, ever. |
Wrong mint or recipient leave the payment open, so a correct transfer can still complete it. An underpaid or overpaid transfer that carries the reference means funds moved incorrectly: the payment becomes failed for you to review and refund.
Commitment levels#
| Status | Commitment | Guidance |
|---|---|---|
submitted | signature known | Not proof of anything. Don't fulfil. |
confirmed | confirmed (supermajority voted) | Fulfil digital goods and most orders. |
finalized | finalized (rooted) | Irreversible. Use for high-value or irreversible fulfilment. |
How payments are found#
- Wallet checkout — Muny builds the exact transfer (amount, mint, recipient, reference) with
POST /v1/checkout/:id/transaction. A tampered transaction simply won't verify. - Signature hints — after the wallet sends, checkout reports the signature with
POST /v1/checkout/:id/submit. It only speeds up detection; a hint without this payment's reference is ignored. - Reference search — background workers search the chain for each open payment's reference key, so QR payments complete even if nobody reports back.
- Late payments — after expiry Muny keeps watching for 24 hours. A valid late transfer is recorded as
confirmedwithlate_confirmation: true, because the money is really in your wallet.
Protections#
- Amount, receiving wallet and mint are stored on the server and can't be changed by the customer.
- Signatures can't be replayed across payments (database uniqueness on network + signature).
- Settlement is a compare-and-set state transition, so a payment is confirmed — and webhooked — exactly once, even when several workers race.
- Test and live payments live in separate networks and are never mixed in queries or reports.
- Fake success callbacks and edited redirect URLs have no effect: nothing client-side can change the status.
Non-custodial
Customers pay directly into your settlement wallet. Muny only observes and verifies. It never holds customer funds in transit.Reconcile from your server#
ts
const payment = await muny.paymentIntents.retrieve(id);
switch (payment.status) {
case "confirmed":
case "finalized":
// payment.transaction_signature, payment.payer_address and
// payment.fiat_value_at_payment come from on-chain verification
return fulfil(payment);
case "failed":
return flagForReview(payment.failure_code); // underpaid | overpaid
case "expired":
case "cancelled":
return releaseInventory(payment.reference);
default:
return; // still waiting
}