Skip to main content

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 creditsConsumed to 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 false for isActive()
  • Cannot be executed by keepers
  • Preserves all state for later resumption

On-Chain vs Off-Chain Responsibility

AspectOn-ChainOff-Chain
Allocation limitsEnforced (remainingBatches)
Credit consumptionCheckpoint ledger onlyActual usage tracking
Payment triggersSettlement state (isSettled)Exhaustion detection
Sequence integrityEnforced (replay prevention)
Work manifestsHash storedFull manifest stored
Service deliveryMerchant 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 TypecreditsUsedEffect
Checkpoint< batchAmountUpdates creditsConsumed, service stays active
Exhaustion== batchAmountSets 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:

  1. On-chain state shows last checkpoint (e.g., 999/1000 credits)
  2. The service can request remaining credit
  3. Merchant issues new voucher for full exhaustion
  4. No deadlock—system continues from last known state

Service Crashes

If a service crashes mid-batch:

  1. On-chain state reflects last checkpoint
  2. A new service instance reads creditsConsumed from chain
  3. Continues from checkpoint, not from zero
  4. No double-spending possible due to cumulative tracking

Delayed Submissions

If voucher submission is delayed:

  1. Sequence number prevents replay of old vouchers
  2. The service remains active until exhaustion voucher is submitted
  3. No race condition—sequence locks ensure ordering

Replay Attempts

Each voucher includes:

  • sequence: must match current on-chain sequence
  • creditsUsed: must exceed current creditsConsumed

Old vouchers fail both checks. Duplicate submissions fail the second.

Partial Execution

If a keeper fails mid-execution:

  1. On-chain state is unchanged (transaction reverted)
  2. Next keeper attempt succeeds normally
  3. Idempotency bitmap prevents double-charges

Integration Notes

When to Use Credits vs Subscriptions

Use CreditsUse Subscriptions
Usage varies significantlyPredictable recurring access
Pay-per-use modelFlat-rate model
Service-driven consumptionHuman-driven consumption
Metered APIs, computeSaaS 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 batchAmount in 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.