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:

CheckFailure codeWhy
Same Solana network as the payment (devnet vs mainnet)wrong_networkTest transfers can never pay live payments.
Transaction succeeded (no on-chain error)transaction_failedA failed transaction moved nothing.
Commitment ≥ confirmednot_confirmedUnconfirmed transactions can still be dropped.
Carries this payment's reference keymissing_referenceTies the transfer to exactly one payment.
Correct token mintwrong_mintAnother token isn't MUNY.
Recipient is the settlement wallet ownerwrong_recipientThe destination is fixed at creation.
Recipient's balance grew by exactly the amountunderpaid / overpaidComputed 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#

StatusCommitmentGuidance
submittedsignature knownNot proof of anything. Don't fulfil.
confirmedconfirmed (supermajority voted)Fulfil digital goods and most orders.
finalizedfinalized (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 confirmed with late_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
}