Integrate PayRam into an EXISTING app using the recommended top-up wallet pattern โ credit crypto payments to a user's in-app wallet balance first, then debit invoices from that balance. Handles every crypto payment reality (overpayment, underpayment, multiple sends, late payments, duplicate webhooks) as balance states instead of payment exceptions. Covers the ledger schema, the credit/debit flows, idempotency, concurrency, reconciliation, and PayRam's cumulative filled_amount_in_usd semantics. Use when adding PayRam to an app that has its own users/invoices/orders, when payments may not exactly match invoice amounts, or when building credits, prepaid balances, or usage billing on crypto rails.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Integrate PayRam into an EXISTING app using the recommended top-up wallet pattern โ credit crypto payments to a user's in-app wallet balance first, then debit invoices from that balance. Handles every crypto payment reality (overpayment, underpayment, multiple sends, late payments, duplicate webhooks) as balance states instead of payment exceptions. Covers the ledger schema, the credit/debit flows, idempotency, concurrency, reconciliation, and PayRam's cumulative filled_amount_in_usd semantics. Use when adding PayRam to an app that has its own users/invoices/orders, when payments may not exactly match invoice amounts, or when building credits, prepaid balances, or usage billing on crypto rails.
PayRam Top-Up Wallet Integration (recommended for existing apps)
New store with no payment history? Use the checkout/plugin path (payram-checkout-integration, or the WooCommerce plugin). Existing app with users and invoices? Use THIS pattern.
Why top-up-first, not pay-per-invoice
Crypto payments are approximate by nature. A customer paying a $50 invoice may send $49.20 (gas mental-math), $60 (round number), $50 in two transfers, or the right amount two hours after the link "expired" โ the funds still arrive on-chain. If you bind payments directly to invoices, every one of those is an exception you must code for.
The top-up pattern flips it: whatever arrives is credited to the user's wallet balance โ exactly the amount received. Invoices are then debited from the balance as a plain, atomic app-side operation. Every crypto quirk becomes a balance state, not a payment failure:
Crypto reality
Direct-to-invoice
Top-up wallet
Overpayment (OVER_FILLED)
Refund flow needed
Excess stays as balance for next invoice
Underpayment (PARTIALLY_FILLED)
Failed payment, retry whole amount
Balance credited; top up just the difference
Two partial sends
Manual matching
Both credit; invoice settles when balance suffices
Late payment after "expiry"
Orphaned funds
Still credited; user spends it normally
Refund requested
On-chain refund per payment
App-side reversal entry (+ PayRam payout only if crypto must leave)
Architecture: the ledger
Three tables. The ledger is append-only; the balance is derived (cache it, but the ledger is truth).
CREATE TABLE user_wallets (
id BIGSERIAL PRIMARY KEY,
user_id BIGINTNOT NULLUNIQUE,
balance_usd NUMERIC(20,8) NOT NULLDEFAULT0CHECK (balance_usd >=0),
updated_at TIMESTAMPTZ NOT NULLDEFAULT now()
);
CREATE TABLE wallet_ledger (
id BIGSERIAL PRIMARY KEY,
wallet_id BIGINTNOT NULLREFERENCES user_wallets(id),
entry_type TEXT NOT NULLCHECK (entry_type IN
('credit','credit_adjustment','debit','debit_reversal')),
amount_usd NUMERIC(20,8) NOT NULLCHECK (amount_usd >0),
payram_reference_id TEXT, -- set on credit/credit_adjustment
invoice_id BIGINT, -- set on debit/debit_reversal
memo TEXT,
created_at TIMESTAMPTZ NOT NULLDEFAULT now()
);
-- Idempotency: exactly ONE base credit per PayRam payment reference.CREATEUNIQUE INDEX one_credit_per_reference
ON wallet_ledger (payram_reference_id) WHERE entry_type ='credit';
CREATE TABLE invoices (
id BIGSERIAL PRIMARY KEY,
user_id BIGINTNOT NULL,
amount_usd NUMERIC(20,8) NOT NULL,
status TEXT NOT NULLDEFAULT'pending'CHECK (status IN ('pending','awaiting_funds','paid','cancelled')),
paid_at TIMESTAMPTZ
);
Use NUMERIC, never floats. Everything is denominated in USD โ PayRam's filled_amount_in_usd gives you the USD value of what actually arrived, so a single-currency ledger sidesteps FX entirely.
The critical PayRam semantic: filled amounts are CUMULATIVE
One payment reference can fire several webhooks as funds arrive: PARTIALLY_FILLED (filled_amount_in_usd: 20.00) โ FILLED (filled_amount_in_usd: 50.00). The amount is the running total for that reference, not a delta. Your credit logic must be:
on webhook (reference_id, status, filled_amount_in_usd):
if status not in (PARTIALLY_FILLED, FILLED, OVER_FILLED): ack and ignore
in one DB transaction:
prior = SUM(amount) of credit + credit_adjustment rows for reference_id
delta = filled_amount_in_usd - prior
if delta <= 0: ack (duplicate or out-of-order webhook โ already credited)
insert ledger row:
entry_type = 'credit' if prior == 0 else 'credit_adjustment'
amount_usd = delta, payram_reference_id = reference_id
balance_usd += delta
then: try_settle_open_invoices(user)
This one function absorbs duplicates (delta โค 0), out-of-order delivery, partial-then-full fills, and overpayment โ with zero special cases.
The flows
Flow A โ invoice settlement (spend from balance)
create invoice โ BEGIN; SELECT balance FROM user_wallets WHERE user_id=? FOR UPDATE;
if balance >= invoice.amount:
insert debit row (invoice_id), balance -= amount, invoice.status='paid'; COMMIT
else:
invoice.status='awaiting_funds'; COMMIT โ go to Flow B for the shortfall
FOR UPDATE (or SERIALIZABLE) makes concurrent debits of one wallet safe โ the balance check and the debit are one atomic unit.
Flow B โ top-up (get funds in)
shortfall = invoice.amount - balance
POST {payram}/api/v1/payment (API-Key header)
{ customerEmail, customerID: "<your user_id>", amountInUSD: shortfall, invoiceID: "<your invoice id>" }
โ show returned url to the user
customerID = your user id โ it's how the webhook maps back to the wallet.
invoiceID is optional metadata; the ledger does NOT rely on it (credits are wallet-level).
Ask for the shortfall, not the full invoice โ existing balance already counts.
Flow C โ the credit webhook (funds arrived)
Register your webhook in the PayRam project. PayRam POSTs snake_case JSON with an API-Key header equal to your configured shared secret โ verify it with a constant-time compare. Then run the cumulative-credit logic above, then re-attempt Flow A for any awaiting_funds invoices of that user.
Flow D โ refunds & cancellations
App-level refund (user keeps money in your app): insert debit_reversal for the invoice โ balance goes back up. No crypto moves.
Crypto must actually leave: pay out via PayRam's payout flow (see payram-payouts) AND insert a matching debit (memo: refund payout) so the ledger mirrors reality.
Case matrix (all of them)
#
Case
What happens
1
Exact payment
credit = invoice โ Flow A settles immediately
2
Overpayment
credit > invoice โ invoice paid, excess remains as balance
3
Underpayment
credit < invoice โ invoice awaiting_funds; UI offers top-up link for shortfall
4
Multiple sends, one reference
cumulative webhooks โ base credit + adjustments; settles when total suffices
funds still arrive on-chain โ webhook still fires โ normal credit
7
Duplicate webhook delivery
delta โค 0 โ ignored (retry-safe by construction)
8
Out-of-order webhooks
cumulative math is order-independent
9
Concurrent invoice debits
row lock in Flow A serializes them; CHECK (balance >= 0) is the backstop
10
Refund
reversal entry (app-level) or payout + debit (crypto leaves)
11
Reconciliation drift
nightly job: PayRam payment search (sum filled_amount_in_usd per reference) vs ledger credits โ must match to the cent
PayRam API surface you use
Purpose
Call
Auth
Create top-up link
POST /api/v1/payment{customerEmail, customerID, amountInUSD, invoiceID?}
API-Key header
Check one payment
GET /api/v1/payment/reference/{reference_id}
reference acts as capability
Credit webhook (inbound)
your endpoint receives {reference_id, customer_id, status, filled_amount_in_usd, ...}
verify API-Key shared secret
Reconciliation
POST /api/v1/external-platform/{id}/payment/search (JWT)
dashboard JWT
Statuses that credit: PARTIALLY_FILLED, FILLED, OVER_FILLED. Ignore OPEN; treat CANCELLED as informational (no funds โ no credit).
Generate the code
The MCP tool generate_topup_integration_snippet emits the ledger SQL, the cumulative-credit webhook handler, and the atomic settle function for your framework โ start there, then adapt table names to your app.
Rollout checklist
Create the three tables; wire the webhook endpoint (verify shared secret, constant-time).
Implement cumulative credit + atomic settle (use the generator).
Point a test invoice at testnet, pay the link partially, verify awaiting_funds โ top up โ paid.
Add the nightly reconciliation query before going to mainnet.
Go live; monitor check_node_sync โ a lagging chain delays credits, not correctness.