Usage Credits
The Credits module enables usage-based billing for APIs, compute services, data feeds, and any service where consumption varies. Rather than charging on a fixed schedule, credits let merchants charge based on what users actually consume—without requiring an on-chain transaction for every unit of work.
Who Uses Credits
Any service where consumption is variable and unpredictable benefits from the credits model:
- API providers — charge per call, per request, or per unit of data
- Compute services — charge per inference, per render, or per job
- Data feeds — charge per query or per subscription window
- Autonomous agents — AI agents with bounded spend budgets are a natural fit, since they can consume credits without holding private keys or requiring per-action signatures
What Credits Are
Credits are authorization instruments, not balances.
A credit allocation grants a service permission to consume up to N units of work before the next payment is triggered. The allocation is bounded, revocable, and enforced by on-chain rules—but actual consumption happens off-chain.
This exists because:
- On-chain transactions per API call would be prohibitively expensive
- Services need to meter consumption without an on-chain transaction per unit
- Merchants need enforceable payment guarantees
- Both parties need auditable records
Credits are complementary to subscriptions, not a replacement. Subscriptions trigger on time intervals. Credits trigger on usage exhaustion.
The Envelope Model
Credits are organized into Envelopes—the on-chain authorization container.
An Envelope contains:
- subscriber: the payer (user wallet)
- authorizedAgent: the agent's public key
- planId: reference to the credit plan (price per batch, credits per batch)
- remainingBatches: total batches the user authorized
- sequence: current batch epoch (for replay prevention)
- creditsConsumed: cumulative usage within current batch
- isSettled: whether the batch is exhausted and awaiting payment
The term "envelope" is intentional. It is not a balance. It is a container that holds authorization for a bounded amount of work, with clear start and end conditions.
Credit Lifecycle
1. Allocation
User signs a Permit2 authorization for N batches at a specific price. This creates an Envelope but does not move funds.
User signs Permit2 → Envelope created → remainingBatches = N
2. Initial Payment
The Envelope starts in a "settled" state, signaling it needs funding. A keeper calls PaymentProcessor.execute(), which:
- Transfers the batch price from user to merchant
- Advances the sequence
- Resets
creditsConsumedto 0 - Sets
isSettled = false
The service is now active with a full batch of credits.
3. Active State
The consuming service uses credits off-chain. The merchant tracks usage and delivers service. No on-chain transactions occur during normal operation.
The isActive(subscriber, planId) view function returns true when:
- Envelope exists
- Not paused
isSettled = false(has credits to consume)
4. Checkpointing
Periodically, the service and merchant can record partial usage on-chain via checkpoint vouchers. This is optional but provides:
- Audit trail of work delivered
- Recovery point if the service crashes
- Proof of consumption for disputes
A checkpoint voucher contains:
creditsUsed: cumulative credits consumed (must exceed previous checkpoint)manifestHash: hash of the work manifest- Co-signatures from both user and merchant
Checkpoints update creditsConsumed but do not trigger payment.
5. Exhaustion
When creditsUsed == batchAmount, the batch is fully consumed. The settlement voucher sets isSettled = true, signaling the Envelope is ready for the next payment.
6. Replenishment
A keeper detects isSettled = true and calls PaymentProcessor.execute(). The cycle repeats: payment transfers, sequence advances, credits reset.
7. Pause and Resume
Users can pause their Envelope at any time. A paused Envelope:
- Returns
falseforisActive() - Cannot be executed by keepers
- Preserves all state for later resumption
On-Chain vs Off-Chain Responsibility
| Aspect | On-Chain | Off-Chain |
|---|---|---|
| Allocation limits | Enforced (remainingBatches) | — |
| Credit consumption | Checkpoint ledger only | Actual usage tracking |
| Payment triggers | Settlement state (isSettled) | Exhaustion detection |
| Sequence integrity | Enforced (replay prevention) | — |
| Work manifests | Hash stored | Full manifest stored |
| Service delivery | — | Merchant responsibility |
This split exists because:
- Per-credit on-chain tracking would cost more than the credits are worth
- Merchants already track usage for rate limiting and billing
- On-chain state only needs to record enough to settle disputes
Voucher Settlement
Structure
Settlement vouchers are EIP-712 typed data signed by both parties:
CreditEnvelope(
uint256 id,
uint64 sequence,
uint64 creditsUsed,
bytes32 manifestHash,
uint256 chainId
)
Why Both Signatures?
- User signature: attests to authorizing the consumption
- Merchant signature: attests to delivering the service
This mutual attestation prevents:
- Merchants claiming payment for undelivered work
- Users disputing legitimate consumption
Checkpoint vs Exhaustion
| Voucher Type | creditsUsed | Effect |
|---|---|---|
| Checkpoint | < batchAmount | Updates creditsConsumed, service stays active |
| Exhaustion | == batchAmount | Sets isSettled = true, triggers next payment |
Permissionless Submission
Anyone can submit a valid voucher. The contract validates signatures, not the submitter. This enables:
- Services to self-submit
- Merchants to submit on behalf of services
- Third-party relayers
Failure Modes
Lost Vouchers
If a voucher is lost before submission:
- On-chain state shows last checkpoint (e.g., 999/1000 credits)
- The service can request remaining credit
- Merchant issues new voucher for full exhaustion
- No deadlock—system continues from last known state
Service Crashes
If a service crashes mid-batch:
- On-chain state reflects last checkpoint
- A new service instance reads
creditsConsumedfrom chain - Continues from checkpoint, not from zero
- No double-spending possible due to cumulative tracking
Delayed Submissions
If voucher submission is delayed:
- Sequence number prevents replay of old vouchers
- The service remains active until exhaustion voucher is submitted
- No race condition—sequence locks ensure ordering
Replay Attempts
Each voucher includes:
sequence: must match current on-chain sequencecreditsUsed: must exceed currentcreditsConsumed
Old vouchers fail both checks. Duplicate submissions fail the second.
Partial Execution
If a keeper fails mid-execution:
- On-chain state is unchanged (transaction reverted)
- Next keeper attempt succeeds normally
- Idempotency bitmap prevents double-charges
Integration Notes
When to Use Credits vs Subscriptions
| Use Credits | Use Subscriptions |
|---|---|
| Usage varies significantly | Predictable recurring access |
| Pay-per-use model | Flat-rate model |
| Service-driven consumption | Human-driven consumption |
| Metered APIs, compute | SaaS access, memberships |
Checking Authorization
// On-chain gating
bool allowed = creditModule.isActive(agentAddress, planId);
// Off-chain API check
const envelope = await creditModule.getEnvelope(envId);
const hasCredits = !envelope.isSettled && envelope.creditsConsumed < plan.batchAmount;
Generating Vouchers
Use the generate-credit-voucher.ts script or implement EIP-712 signing:
const types = {
CreditEnvelope: [
{ name: 'id', type: 'uint256' },
{ name: 'sequence', type: 'uint64' },
{ name: 'creditsUsed', type: 'uint64' },
{ name: 'manifestHash', type: 'bytes32' },
{ name: 'chainId', type: 'uint256' },
],
};
const voucher = {
id: envelopeId,
sequence: currentSequence,
creditsUsed: totalConsumed,
manifestHash: keccak256(workManifest),
chainId: BigInt(chainId),
};
const userSig = await userWallet.signTypedData(domain, types, voucher);
const merchantSig = await merchantWallet.signTypedData(domain, types, voucher);
Non-Goals
The following are intentionally not supported:
-
Per-credit on-chain tracking: Would cost more than the credits. Off-chain tracking with on-chain settlement is the correct tradeoff.
-
Credit transfers between users: Credits are authorization, not assets. Transfer the underlying tokens instead.
-
Fractional batch purchases: Batches are the atomic unit. Adjust
batchAmountin the plan for finer granularity. -
Credit expiry: Batches do not expire. Permit2 allowances expire. This keeps the model simple—expiry is handled at the authorization layer, not the credit layer.
Relationship to RAI
Credits are one of several authorization instruments in the RAI model:
- Same non-custodial guarantees (no vaults, no pooled balances)
- Same Permit2 delegation model
- Same PaymentProcessor execution
- Different trigger: exhaustion instead of time
The system treats subscriptions and credits as peers. A merchant can offer both. A user can hold both. The PaymentProcessor executes both using the same validation and fee routing logic.