Credits Model
stripe402 uses a prepaid credits system instead of charging per request.
Why credits?
Stripe imposes these constraints on charges:
Minimum charge: $0.50 USD
Fixed fee: ~$0.30 per transaction (plus percentage)
If an API call costs $0.01, charging per request would mean paying $0.30+ in fees on a $0.01 charge — a 3,000% overhead. And charges below $0.50 aren't even possible.
How it works
Instead of charging per request, stripe402 charges once for a block of credits:
Client requests a resource that costs 100 units ($0.01)
Server responds with 402: "This costs 100 units. Minimum top-up is 50,000 units ($5.00)."
Client pays $5.00 → receives 50,000 units of credit
Server deducts 100 units for this request → 49,900 units remaining
Client makes 499 more requests using their client ID → balance reaches 0
Server issues a new 402 to prompt a top-up
On a $5.00 top-up, Stripe fees are ~$0.45 (2.9% + $0.30). That's 0.09¢ per request when amortized over 500 requests at $0.01 each — economically viable.
Default configuration
DEFAULT_MIN_TOP_UP
50,000 units ($5.00)
The minimum charge per top-up
Per-route minTopUp
Configurable
Override per route via RouteConfig.minTopUp
The minimum top-up should always be at least 500 units ($0.50) to satisfy Stripe's minimum charge requirement.
Balance storage
Client balances are stored in the persistence layer (Stripe402Store):
Redis: Balance stored as a field in a hash at
stripe402:client:{clientId}PostgreSQL: Balance stored as an
INTEGERcolumn instripe402_clients
All balance operations are atomic to prevent double-spending under concurrent requests:
Redis: Uses a Lua script that checks and deducts in a single operation
PostgreSQL: Uses
UPDATE ... WHERE balance >= amount RETURNING balance
Top-up flow
When a client sends a paymentMethodId in the payment header:
Server derives the
clientIdfrom the card's fingerprint (HMAC-SHA256)Server checks if that
clientIdalready has sufficient creditsIf credits are sufficient: deducts the request cost and serves the resource — no charge is created
If credits are insufficient (or client has no balance): a. Server validates
topUpAmount >= minTopUpb. Server charges the card via Stripe (PaymentIntent.createwithconfirm: true) c. Server callsstore.addBalance(clientId, topUpAmount)to credit the balance d. Server deducts the cost of the current request from the new balance e. Server returnscreditsRemainingin thepayment-responseheader
This means clients can safely send the same paymentMethodId on every request without being charged multiple times. The card is only charged when the balance is actually insufficient, making the protocol tolerant of simple clients that don't track clientId separately.
Deduction flow
When a client has existing credits:
Client sends
clientIdin thepaymentheaderServer calls
store.deductBalance(clientId, amount)atomicallyIf balance >= amount: deduction succeeds, remaining balance is returned
If balance < amount: deduction fails (returns
null), server sends 402 witherror: 'insufficient_credits'
Transaction logging
Both store implementations support optional transaction logging via store.recordTransaction(). Each transaction records:
id
string
UUID generated per transaction
clientId
string
The client's identifier
type
'topup' | 'deduction'
Whether credits were added or spent
amount
number
Amount in units
stripePaymentIntentId
string?
Stripe PI ID (top-ups only)
resource
string?
Route key, e.g., 'GET /api/joke' (deductions only)
createdAt
Date
Timestamp
Last updated