Skip to main content

CreditModule

Inherits: ReentrancyGuard, IExecutableModule, IRegistrableModule, IAuthorizationModule, EIP712, IInitializableModule, IMigratableModule

Title: CreditModule

Credit-based RAI module using off-chain vouchers (Envelopes) to trigger JIT refills.

State Variables

_initialized

bool private _initialized

merchant

address public merchant

paymentProcessor

address public paymentProcessor

_nextPlanId

uint256 private _nextPlanId

_nextEnvId

uint256 private _nextEnvId

_plans

mapping(uint256 => PlanPacked) internal _plans

_envelopes

mapping(uint256 => EnvelopePacked) internal _envelopes

envelopeOf

mapping(address => mapping(uint256 => uint256)) public envelopeOf

ENVELOPE_TYPEHASH

EIP-712 typehash for credit envelope settlement vouchers.

chainId is included as an explicit field in the signed struct to prevent cross-chain replay even if two deployments share the same contract address (e.g. CREATE2 with identical salt across chains). OpenZeppelin's EIP712 domain separator also includes chainId, but this provides defence-in-depth.

bytes32 public constant ENVELOPE_TYPEHASH = keccak256(
"CreditEnvelope(uint256 id,uint64 sequence,uint64 creditsUsed,bytes32 manifestHash,uint256 chainId)"
)

MAX_BATCH_QUERY

Maximum number of plan IDs accepted by batch view helpers (OOG guard).

uint256 internal constant MAX_BATCH_QUERY = 256

Functions

constructor

Note: oz-upgrades-unsafe-allow: constructor

constructor() EIP712("amser_CreditModule", "1");

initialize

function initialize(address _merchant, address _paymentProcessor) external;

setPaymentProcessor

Updates the PaymentProcessor address (migration path).

Only the current PaymentProcessor can call this, via migrateModule(). This enables protocol-level upgrades without breaking existing modules.

function setPaymentProcessor(address _newProcessor) external;

Parameters

NameTypeDescription
_newProcessoraddressAddress of the new PaymentProcessor

createPlan

function createPlan(
uint256 price,
uint64 batchAmount,
address token,
bytes32 metadataHash
)
external
returns (uint256 planId);

togglePlanActive

Merchant toggles plan active/inactive.

function togglePlanActive(uint256 planId) external;

onlySubscriber

Modifier to ensure only the subscriber can call pause/resume functions

modifier onlySubscriber(uint256 envId) ;

pauseEnvelope

Pauses an envelope.

Only the subscriber can pause their envelope.

function pauseEnvelope(uint256 envId) external onlySubscriber(envId);

Parameters

NameTypeDescription
envIduint256Envelope ID

resumeEnvelope

Resumes a paused envelope.

Only the subscriber can resume their envelope.

function resumeEnvelope(uint256 envId) external onlySubscriber(envId);

Parameters

NameTypeDescription
envIduint256Envelope ID

updateAllowanceExpiry

Updates the Permit2 allowance expiry for an envelope.

Only the subscriber can extend their envelope's allowance expiry. This allows recovery when an envelope's original expiry has passed but the subscriber has renewed their Permit2 allowance off-chain. Without this, remaining batches would be permanently stranded once the expiry timestamp is exceeded.

function updateAllowanceExpiry(
uint256 envId,
uint256 newExpiry
)
external
onlySubscriber(envId);

Parameters

NameTypeDescription
envIduint256Envelope ID
newExpiryuint256New allowance expiry timestamp (must be in the future and fit in uint48)

updateAuthorizedAgent

Rotates the authorized agent address for an envelope.

Callable by the subscriber (envelope owner) or the merchant. Updates the envelopeOf mapping atomically:

  • Old agent address is removed from envelopeOf
  • New agent address is mapped to this envelope The envelope's sequence, creditsConsumed, remainingBatches, and all other state is preserved. Only the agent address changes. Reverts if newAgent is already the authorized agent (no-op guard). Reverts if newAgent already has an envelope for this plan (would create a mapping collision).
function updateAuthorizedAgent(uint256 envId, address newAgent) external;

Parameters

NameTypeDescription
envIduint256Envelope ID to update
newAgentaddressNew authorized agent address

registerCreditsWithPermit

function registerCreditsWithPermit(
address agent,
uint256 planId,
uint256 totalBatches,
uint256 allowanceExpiry,
bytes calldata permitData,
bytes calldata signature
)
external
nonReentrant
returns (uint256 envId);

settleEnvelope

Submit co-signed proof of credit usage (checkpoint or exhaustion).

Allows cumulative checkpoint updates. Only triggers payment when fully exhausted.

PERMISSIONLESS BY DESIGN This function is callable by anyone who holds valid co-signed proofs (userSig + merchantSig). This is intentional: restricting to authorised callers would create a liveness risk where disputed or abandoned envelopes become permanently unresolvable if no authorised party is willing to pay gas. FRONT-RUNNING RISK (accepted): A mempool observer could front-run a settlement transaction by replaying the same signatures. The impact is limited to timing manipulation — an attacker cannot redirect funds, only control when isSettled becomes true and therefore when keeper execution is triggered. FUTURE ENHANCEMENT: Consider a timelock pattern where authorised parties (subscriber, merchant, authorizedAgent) can settle immediately, but any other caller must wait a SETTLEMENT_DISPUTE_WINDOW (e.g. 7 days) after the last checkpoint. This provides a neutral resolution path for abandoned envelopes without removing the liveness guarantee.

PAYMENT TRIGGER: Payment (keeper execution) is triggered only when creditsConsumed reaches plan.batchAmount (full batch exhaustion). Intermediate settlement checkpoints update creditsConsumed for accounting and audit purposes but do NOT trigger payment. IMPLICATION: If an agent consumes a partial batch and stops (e.g. 80 of 100 credits), the merchant has delivered service but does not receive payment until the full batch is consumed. The merchant co-signs each checkpoint and is therefore aware of consumption state. FUTURE: Proportional payment on settlement (amount * creditsConsumed / batchAmount) is a planned enhancement that would resolve this incentive asymmetry. Track in roadmap.

PAUSED ENVELOPES: Settlement is intentionally permitted on paused envelopes. Pausing controls keeper execution (billing), not settlement (recording already-consumed credits). Preventing settlement on paused envelopes would create a liveness risk where off-chain credit consumption could never be recorded on-chain, permanently blocking the merchant from receiving payment for delivered service.

function settleEnvelope(
uint256 id,
uint64 sequence,
uint64 creditsUsed,
bytes32 manifestHash,
bytes calldata userSig,
bytes calldata merchantSig
)
external
nonReentrant;

Parameters

NameTypeDescription
iduint256Envelope ID
sequenceuint64Current batch sequence (must match on-chain state)
creditsUseduint64Cumulative credits used in this batch (must be > current creditsConsumed)
manifestHashbytes32Hash of work manifest for this checkpoint
userSigbytesUser's EIP-712 signature
merchantSigbytesMerchant's EIP-712 signature

registrationContext

function registrationContext(
uint256 planId,
address
)
external
view
override
returns (address token, uint256 minAmount);

registerExecution

function registerExecution(
address subscriber,
uint256 planId,
uint256 executionBudget,
uint256 allowanceExpiry
)
external
override
returns (uint256 id);

quoteExecution

Quotes whether execution is allowed and returns execution parameters.

Single entry point replacing canExecute, getExecutionFailureReason, and executionContext. Maps envelope-specific failure states to standardized ExecutionFailureReason enum. Failure reasons are checked in priority order:

  1. Envelope existence (permanent failure)
  2. Paused state (user-controlled)
  3. Plan active state (merchant-controlled)
  4. Remaining batches (user-controlled)
  5. Settlement status (credits exhausted triggers refill) When reason == None, the remaining return values contain valid execution context. When reason != None, context values are zeroed out.
function quoteExecution(uint256 id)
external
view
override
returns (
ExecutionFailureReason reason,
address payer,
address recipient,
address token,
uint256 amount,
uint256 executionTime,
uint256 windowId
);

Parameters

NameTypeDescription
iduint256Envelope ID

Returns

NameTypeDescription
reasonExecutionFailureReasonStandardized failure reason (None if executable)
payeraddressAddress that will pay (subscriber)
recipientaddressAddress that will receive payment (merchant)
tokenaddressToken address to transfer
amountuint256Amount to transfer (plan price)
executionTimeuint256Canonical execution timestamp (block.timestamp for credits)
windowIduint256Idempotency window identifier (sequence number)

onExecute

function onExecute(uint256 id, uint256) external override;

isActive

function isActive(address subject, uint256 planId) external view override returns (bool);

isActiveAny

Optional batch helper: true if any scope is active under current CreditModule semantics.

Array length is bounded by MAX_BATCH_QUERY to prevent OOG griefing. Non-existent plan IDs are safely skipped (_isActive returns false).

function isActiveAny(
address subject,
uint32[] calldata scopes
)
external
view
override
returns (bool);

isActiveBatch

Optional batch helper: returns isActive for each scope in order.

Array length is bounded by MAX_BATCH_QUERY to prevent OOG griefing. Non-existent plan IDs are safely skipped (_isActive returns false).

function isActiveBatch(
address subject,
uint32[] calldata scopes
)
external
view
returns (bool[] memory states);

_isActive

Returns false if the associated plan has been deactivated by the merchant.

function _isActive(address subject, uint256 planId) internal view returns (bool);

getEnvelope

Get envelope details by ID

function getEnvelope(uint256 id)
external
view
returns (
uint64 sequence,
bool isSettled,
uint32 planId,
uint32 remainingBatches,
address authorizedAgent,
address subscriber,
uint64 creditsConsumed,
bool paused,
uint48 allowanceExpiry
);

Parameters

NameTypeDescription
iduint256Envelope ID

Returns

NameTypeDescription
sequenceuint64Current batch epoch
isSettledboolTrue if exhausted and waiting for PaymentProcessor
planIduint32The credit plan ID
remainingBatchesuint32Total batches remaining
authorizedAgentaddressThe AI Agent public key
subscriberaddressThe payer address
creditsConsumeduint64Cumulative credits consumed in current batch
pausedboolTrue if envelope is paused by subscriber
allowanceExpiryuint48

getPlan

Get plan details by ID

function getPlan(uint256 planId)
external
view
returns (
uint160 price,
uint64 batchAmount,
bool active,
address token,
bytes32 metadataHash
);

Parameters

NameTypeDescription
planIduint256Plan ID

Returns

NameTypeDescription
priceuint160Cost per refill
batchAmountuint64Credits granted per refill
activeboolWhether the plan is active
tokenaddressPayment token address
metadataHashbytes32Metadata hash

Events

PlanCreated

event PlanCreated(
uint256 indexed planId, uint256 price, uint256 batchAmount, bytes32 metadataHash
);

EnvelopeCreated

event EnvelopeCreated(
uint256 indexed id,
address indexed subscriber,
address indexed agent,
uint256 planId,
uint256 remainingBatches
);

EnvelopeSettled

event EnvelopeSettled(
uint256 indexed id, uint64 sequence, bytes32 manifestHash, uint64 creditsUsed
);

CreditsRefilled

event CreditsRefilled(uint256 indexed id, uint64 newSequence, uint256 remainingBatches);

PlanActiveToggled

event PlanActiveToggled(uint256 indexed planId, bool active);

EnvelopePaused

event EnvelopePaused(uint256 indexed id);

EnvelopeUnpaused

event EnvelopeUnpaused(uint256 indexed id);

AllowanceExpiryUpdated

event AllowanceExpiryUpdated(uint256 indexed envId, uint256 newExpiry);

AuthorizedAgentUpdated

event AuthorizedAgentUpdated(
uint256 indexed envId, address indexed oldAgent, address indexed newAgent
);

PaymentProcessorUpdated

event PaymentProcessorUpdated(address indexed oldProcessor, address indexed newProcessor);

Errors

AlreadyInitialized

error AlreadyInitialized();

OnlyMerchant

error OnlyMerchant();

OnlyProcessor

error OnlyProcessor();

OnlySubscriber

error OnlySubscriber();

InvalidSignatures

error InvalidSignatures();

AlreadySettled

error AlreadySettled();

SequenceMismatch

error SequenceMismatch();

UsageMustIncrease

error UsageMustIncrease();

ExceedsBatchLimit

error ExceedsBatchLimit();

PlanDoesNotExist

error PlanDoesNotExist();

PlanNotActive

error PlanNotActive();

EnvelopeAlreadyPaused

error EnvelopeAlreadyPaused();

EnvelopeNotPaused

error EnvelopeNotPaused();

InvalidToken

error InvalidToken();

InvalidPrice

error InvalidPrice();

InvalidBatchAmount

error InvalidBatchAmount();

EnvelopeAlreadyExistsForPlan

error EnvelopeAlreadyExistsForPlan();

InvalidExecutionBudget

error InvalidExecutionBudget();

EnvelopeDoesNotExist

error EnvelopeDoesNotExist();

ArrayTooLong

error ArrayTooLong();

InvalidProcessor

error InvalidProcessor();

InvalidMerchant

error InvalidMerchant();

InvalidAgent

error InvalidAgent();

NoBatchesRemaining

error NoBatchesRemaining();

InvalidAllowanceExpiry

error InvalidAllowanceExpiry();

AgentAlreadyAuthorized

error AgentAlreadyAuthorized();

OnlySubscriberOrMerchant

error OnlySubscriberOrMerchant();

Structs

PlanPacked

Plan configuration for credit refills. Note: metadataHash is stored as a fixed-size value for compact storage.

struct PlanPacked {
uint160 price; // Cost per refill
uint64 batchAmount; // Credits granted per refill
bool active;
address token;
bytes32 metadataHash;
}

EnvelopePacked

Slot 0: sequence (64 bits) | isSettled (1 bit) | paused (1 bit) | planId (32 bits) | remainingBatches (32 bits) | allowanceExpiry (48 bits) Slot 1: authorizedAgent (160 bits) Slot 2: subscriber (160 bits) Slot 3: creditsConsumed (64 bits) - cumulative usage in current batch

struct EnvelopePacked {
uint64 sequence; // Current batch epoch
bool isSettled; // True if exhausted and waiting for PaymentProcessor
bool paused; // True if envelope is paused by subscriber
uint32 planId; // The credit plan
uint32 remainingBatches; // Total batches allowed (execution guardrail)
uint48 allowanceExpiry; // When the delegated Permit2 allowance expires
address authorizedAgent; // The AI Agent public key
address subscriber; // The payer
uint64 creditsConsumed; // Cumulative usage in current batch (checkpoint tracking)
}