--- eip: 8162 title: Agent Subscription description: An interface for managing subscriptions to onchain AI agent services with cycle-based pricing and direct payment. author: Andrei Sambra (@deiu) discussions-to: https://ethereum-magicians.org/t/erc-8162-agent-subscription-protocol/27751 status: Draft type: Standards Track category: ERC created: 2026-02-16 requires: 20, 712, 2612, 8004 --- ## Abstract This proposal defines an interface for managing subscriptions to onchain AI agent services. It enables users (humans or agents) to subscribe to tiered service plans offered by agents registered via [ERC-8004](./eip-8004.md), with payments in [ERC-20](./eip-20.md) tokens. The protocol uses a cycle-based prepaid model where subscribers pay upfront for one or more billing cycles and tokens are transferred directly to the agent owner. The registry contract never holds funds. Subscription state lives entirely onchain, requiring zero offchain billing infrastructure from agent developers. An HTTP 402 access flow using [EIP-712](./eip-712.md) signatures allows subscribers to prove access without revealing their identity in the initial request. ## Motivation The agentic economy is converging on two complementary primitives: identity ([ERC-8004](./eip-8004.md)) and payments. Per-request payment protocols like x402 solve the transactional case well, but many agent interactions are better served by a subscription model. An AI coding assistant used daily, a monitoring agent that checks a portfolio every hour, or a team of agents collaborating on a task all benefit from predictable pricing and persistent access rather than per-call negotiation. Users can iterate on multi-turn conversations without tracking per-message costs, and agents can serve requests without settling a payment on every call. No existing standard covers recurring, tiered access to agent services. This ERC fills the gap with a protocol that: 1. Requires zero offchain infrastructure from agent developers. Subscribe, renew, and access verification logic is handled entirely onchain. 2. Integrates with [ERC-8004](./eip-8004.md) for agent identity and discovery. 3. Supports multiple plan tiers per agent, enabling differentiated service levels. 4. Gives users full control with cycle-based prepaid billing, where subscriptions simply expire at the end of the paid period. Agents can support both x402 and this protocol simultaneously, using per-request billing for occasional usage and subscriptions for regular access. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ### Overview The protocol consists of a singleton **SubscriptionRegistry** contract deployed per chain. At deployment, the registry is configured with the address of the local [ERC-8004](./eip-8004.md) Identity Registry on the same chain. This address is immutable and used for all agent ownership verification: when an agent owner calls `createPlan()`, the registry calls `identityRegistry.ownerOf(agentId)` to confirm the caller owns the agent NFT. Agents MUST be registered on the same chain as the SubscriptionRegistry they use. An agent registered on Base that wants subscriptions on Base uses the Base SubscriptionRegistry. If the same agent also wants subscriptions on Arbitrum, it registers on Arbitrum's [ERC-8004](./eip-8004.md) Identity Registry and uses Arbitrum's SubscriptionRegistry. The registry interacts with: - An [ERC-8004](./eip-8004.md) Identity Registry (same chain) for agent ownership verification. - [ERC-20](./eip-20.md) tokens (e.g., USDC) for payment. ``` +--------------------------------------------------------------+ | Offchain (Agent Card) | | | | ERC-8004 agentURI JSON: | | +-- subscriptions: | | +-- registryChain: "eip155:8453" | | +-- registryAddress: "0x..." | | +-- plans: | | +-- { planId: 1, name: "Starter", ... } | | +-- { planId: 2, name: "Pro", ... } | | +-- { planId: 3, name: "Enterprise", ... } | | | +-------------------------------+-------------------------------+ | v +--------------------------------------------------------------+ | Onchain (SubscriptionRegistry, singleton) | | | | Configured with: identityRegistry address (immutable) | | | | Plans: createPlan / updatePlan / deactivatePlan | | Subscriptions: subscribe / renew | | Access: verifyAccess / isActive | | Payments: direct transfer to agent owner | | | +-------------------------------+-------------------------------+ | v +--------------------------------------------------------------+ | Payment Layer (ERC-20) | | | | Token transfers via: | | +-- EIP-2612 permit + transferFrom (e.g., USDC) | | +-- Standard ERC-20 approve + transferFrom | | | +--------------------------------------------------------------+ ``` ### Pricing Model Plans use a cycle-based price. Each plan defines a `price` for a fixed `cycleDuration`, and subscribers pay for one or more cycles upfront. Total cost = `price * cycles`. Total duration = `cycleDuration * cycles`. Example: A plan priced at 5 USDC/month (USDC has 6 decimals): - `price = 5000000` (5 USDC in base units) - `cycleDuration = 2592000` (30 days in seconds) - 1-month subscription: `5000000 * 1 = 5000000` (5 USDC) - 1 year (12-month) subscription: `5000000 * 12 = 60000000` (60 USDC) ### Direct Payment Model Subscription payments are transferred directly to the agent owner (via `identityRegistry.ownerOf(agentId)`) at the time of subscribe or renew. The registry contract never holds funds. **Subscribe / Renew**: The subscriber pays `price * cycles` and the tokens are transferred immediately to the agent owner. The subscription is active from the start time until the end time. Subscriptions are prepaid and simply expire at `endTime`. Once paid, the service runs until the end of the billing period. This matches user expectations: paying for a month of service means getting a month of service. ### Data Structures #### Plan ```solidity struct Plan { address asset; // payment token address (address(0) = plan does not exist) uint256 price; // price per cycle in token base units uint32 cycleDuration; // duration of one billing cycle in seconds bool active; // whether new subscriptions are accepted } ``` - Plans are stored by a key derived from `(agentId, planId)`. Plan `(agentId=5, planId=1)` and `(agentId=9, planId=1)` are distinct. - A plan exists if `asset != address(0)`. No separate `exists` flag is needed. - `price` uses `uint256` to support any token denomination without overflow concerns. - `cycleDuration` defines the length of one billing cycle in seconds (e.g., 2592000 for 30 days). #### Subscription ```solidity struct Subscription { uint256 agentId; // ERC-8004 agent reference uint32 planId; // active plan tier address subscriber; // the subscribing address uint48 startTime; // subscription start timestamp uint48 endTime; // subscription end timestamp } ``` - Subscriptions are stored by `subscriptionId = keccak256(subscriber, agentId, planId, nonce)`. Hash-based IDs prevent enumeration of subscriber counts while remaining deterministic and verifiable. ### Events The registry MUST emit the following events: - `PlanCreated(uint256 indexed agentId, uint32 indexed planId, address asset, uint256 price, uint32 cycleDuration)` when a plan is created. - `PlanUpdated(uint256 indexed agentId, uint32 indexed planId, uint256 newPrice, uint32 newCycleDuration)` when a plan's pricing is updated. - `PlanDeactivated(uint256 indexed agentId, uint32 indexed planId)` when a plan is deactivated. - `Subscribed(bytes32 indexed subscriptionId, uint256 indexed agentId, uint32 planId, address indexed subscriber, uint48 startTime, uint48 endTime, uint256 amount)` when a new subscription is created. - `Renewed(bytes32 indexed subscriptionId, uint48 newEndTime)` when a subscription is renewed. These events are the primary mechanism for agents to maintain a local cache of subscription state (see Caching Recommendation). ### Configuration The registry MUST expose the identity registry address it was deployed with: ```solidity function identityRegistry() external view returns (address); ``` This value is set at deployment and MUST be immutable. It is used for all agent ownership verification via `identityRegistry.ownerOf(agentId)`. ### Plan Management Agent owners manage plans through three functions. The caller MUST be the owner of the [ERC-8004](./eip-8004.md) agent NFT for the given `agentId`. To create a plan, the agent owner calls: ```solidity function createPlan( uint256 agentId, uint32 planId, address asset, uint256 price, uint32 cycleDuration ) external; ``` The `planId` MUST be unique within the scope of `agentId`. Both `price` and `cycleDuration` MUST be greater than zero. The `asset` MUST be a standard [ERC-20](./eip-20.md) token; fee-on-transfer and rebasing tokens are NOT supported. A `PlanCreated` event MUST be emitted. To update pricing on an existing plan: ```solidity function updatePlan( uint256 agentId, uint32 planId, uint256 newPrice, uint32 newCycleDuration ) external; ``` Only `price` and `cycleDuration` are mutable. To change the payment `asset`, the agent owner creates a new plan. Changes apply to new subscriptions and future renewals; active subscriptions are unaffected until they renew. A `PlanUpdated` event MUST be emitted. To stop accepting new subscriptions on a plan: ```solidity function deactivatePlan(uint256 agentId, uint32 planId) external; ``` Existing subscriptions MUST remain active until expiry. Active subscriptions MAY still be renewed (extended). Expired subscriptions MUST NOT be renewed on a deactivated plan, since an expired renewal is effectively a new subscription. A `PlanDeactivated` event MUST be emitted. Plan details MUST be queryable: ```solidity function getPlan(uint256 agentId, uint32 planId) external view returns (address asset, uint256 price, uint32 cycleDuration, bool active); ``` ### Subscription Lifecycle #### Subscribe A user subscribes by calling: ```solidity function subscribe(uint256 agentId, uint32 planId, uint32 cycles) external returns (bytes32 subscriptionId); ``` The plan MUST be active and `cycles` MUST be >= 1. The caller MUST have approved the registry to transfer `price * cycles` tokens. Tokens are transferred directly to the agent owner (via `identityRegistry.ownerOf(agentId)`) using `safeTransferFrom`. If the subscriber already has an active subscription to the same agent and plan, the call MUST revert. A `Subscribed` event MUST be emitted. A permit variant combines [EIP-2612](./eip-2612.md) approval and subscription in a single transaction: ```solidity function subscribeWithPermit( uint256 agentId, uint32 planId, uint32 cycles, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external returns (bytes32 subscriptionId); ``` ``` User SubscriptionRegistry ERC-20 Token | | | |-- subscribe(agentId, planId, cyc) ->| | | |-- safeTransferFrom(user, owner, amt)->| | |<-- success ---------------------------| | | [create subscription] | |<-- subscriptionId ------------------| | ``` The agent owner is not a participant in this call. The Registry instructs the [ERC-20](./eip-20.md) contract to debit the subscriber and credit the agent owner; the owner's balance increases in the token's internal state, but no contract call is made to the owner's address. #### Renew An existing subscription is extended by calling: ```solidity function renew(bytes32 subscriptionId, uint32 cycles) external; ``` The subscription MUST exist. `cycles` MUST be >= 1. Payment is charged at the plan's current price (indicated in the agent card), so renewals reflect any price changes made by the agent owner. If the subscription is still active, the renewal begins from the current `endTime`. If the subscription is expired, it resets. In other words, `startTime` is set to now, `endTime` is set to `now + (cycleDuration * cycles)`. In the expired case, the plan MUST be active and the call MUST revert if another active subscription already exists for the same `(subscriber, agentId, planId)`. Tokens are transferred directly to the agent owner. A `Renewed` event MUST be emitted. A permit variant is also provided: ```solidity function renewWithPermit( bytes32 subscriptionId, uint32 cycles, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; ``` ``` User SubscriptionRegistry ERC-20 Token | | | |-- renew(subscriptionId, cycles) --->| | | | [if active: extend from endTime] | | | [if expired: reset startTime] | | |-- safeTransferFrom(user, owner, amt)->| | |<-- success ---------------------------| |<-- success -------------------------| | ``` ### Access Verification The registry provides two view functions for checking subscription status. To check whether a specific subscription is currently active: ```solidity function isActive(bytes32 subscriptionId) external view returns (bool); ``` A subscription is active if it exists and `block.timestamp` is between `startTime` and `endTime` (inclusive). To check whether a subscriber has access to a given agent (optionally restricted to a specific plan): ```solidity function verifyAccess(address subscriber, uint256 agentId, uint32 planId) external view returns (bool); ``` If `planId` is `0`, the function returns `true` if the subscriber has any active subscription to the agent, regardless of tier. This is the primary function used by agents during the HTTP 402 verification flow. Full subscription details are available via: ```solidity function getSubscription(bytes32 subscriptionId) external view returns ( uint256 agentId, uint32 planId, address subscriber, uint48 startTime, uint48 endTime ); ``` ### Same-Chain Identity Binding Each SubscriptionRegistry is deployed with an immutable reference to the [ERC-8004](./eip-8004.md) Identity Registry on the same chain. An agent can register on multiple chains, each with its own independent identity registry, subscription registry, and subscription pool. Cross-chain identity verification would require oracles, bridges, or attestations, each adding a trust assumption and failure mode. Same-chain binding is simple and verifiable: the registry calls `identityRegistry.ownerOf(agentId)` directly. When a client proves its subscription (in response to a 402), it includes the `registryChain` and `registryAddress` in the signed proof. The agent validates that the specified registry is one it recognizes, then verifies the subscription on that chain. ### Hash-Based Subscription IDs Subscription IDs are derived from `keccak256(subscriber, agentId, planId, salt)`, not sequential integers. Sequential IDs leak information. An observer can trivially determine subscriber count, growth rate, and ordering. Hash-based IDs provide the same uniqueness and determinism without the information leakage. The trade-off (inability to enumerate subscriptions by ID range) is acceptable because the contract provides query functions for access verification. ### Immutable Subscription Terms When a subscription is created, the price paid covers the full subscription period. The subscription struct stores `startTime` and `endTime`. The subscriber pays `price * cycles` upfront and receives access for that window. - **Current cycle**: the subscriber paid for it. The price is locked for that period regardless of any plan changes. - **Renewals**: always use the plan's current price at renewal time. If the agent updates pricing between cycles, the new price applies to the next renewal. - **No mid-period surprises**: an agent cannot retroactively change the terms of an active subscription. The subscriber's access runs until `endTime` no matter what. ### Agent Card Extension Agents that support subscriptions SHOULD advertise their plans in the [ERC-8004](./eip-8004.md) agent registration file (resolved via `agentURI`). The following extension to the agent card schema is RECOMMENDED: ```json { "type": "agent", "name": "Example Agent", "services": [], "subscriptions": [ { "chain": "eip155:8453", "registryAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "agentId": 42, "plans": [ { "planId": 1, "name": "Starter", "description": "Basic access: 100 requests/day", "price": "5000000", "cycleDuration": 2592000, "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "features": ["100 requests/day", "Standard response time"] }, { "planId": 2, "name": "Pro", "description": "Full access: unlimited requests", "price": "20000000", "cycleDuration": 2592000, "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "features": ["Unlimited requests", "Priority response time"] } ] } ] } ``` This allows clients, agent directories, and frontend applications to discover available plans without querying the chain, while the onchain registry remains the source of truth for pricing and access verification. ### HTTP 402 Access Flow Subscription onboarding begins with discovery: the client fetches the agent's [ERC-8004](./eip-8004.md) agent card, which advertises available plans, registries, and pricing. Using this information, the client subscribes onchain before making any authenticated requests. The 402 flow described below handles access control, gating requests from clients that have already subscribed. Similar to x402, this protocol uses two custom HTTP headers: `SUBSCRIPTION-REQUIRED` (agent to client) and `SUBSCRIPTION-SIGNATURE` (client to agent). #### Flow Overview ``` Client Agent Service SubscriptionRegistry | | | |-- GET /api/resource ----------------->| | | | [no subscription proof] | |<-- 402 + SUBSCRIPTION-REQUIRED -------| | | | | | [client signs EIP-712 proof] | | | | | |-- GET /api/resource ----------------->| | | + SUBSCRIPTION-SIGNATURE header | | | |-- ecrecover(signature) ---+ | | | [recover subscriber] | | | |<--------------------------+ | | |-- verifyAccess(addr,agent,plan)->| | |<-- true -------------------------| |<-- 200 OK (resource data) ------------| | ``` #### Step 1: Initial Request The client sends a standard HTTP request to the agent's service endpoint. No wallet address, subscription ID, or authentication is included. ```http GET /api/chat HTTP/1.1 Host: agent.example.com Content-Type: application/json ``` #### Step 2: Agent Responds with 402 and SUBSCRIPTION-REQUIRED If the request lacks a `SUBSCRIPTION-SIGNATURE` header, the agent responds with HTTP 402 and a `SUBSCRIPTION-REQUIRED` header containing a base64-encoded JSON object: ```http HTTP/1.1 402 Payment Required SUBSCRIPTION-REQUIRED: eyJ0eXBlIjoic3Vic2Ny... ``` Decoded `SUBSCRIPTION-REQUIRED` payload: ```json { "type": "subscription", "registries": [ { "chain": "eip155:8453", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "agentId": 42 } ], "challenge": "0x1a2b3c4d..." } ``` | Field | Required | Description | |-------|----------|-------------| | `type` | MUST | Always `"subscription"`. | | `registries` | MUST | Array of `{ chain, address, agentId }` objects identifying the SubscriptionRegistry contracts and agent identities the agent accepts. Each `chain` is a CAIP-2 chain identifier, `address` is the registry contract address on that chain, and `agentId` is the [ERC-8004](./eip-8004.md) agent identifier on that chain's identity registry. The `agentId` MAY differ across chains. | | `challenge` | MAY | Opaque bytes provided by the agent that the client MUST include in the signed data if present. Agents can use this for replay protection (nonce), freshness (timestamp), session binding, or any other agent-specific validation logic. If absent, the client signs with an empty challenge (`0x`). | #### Step 3: Client Signs [EIP-712](./eip-712.md) Proof The client constructs and signs an [EIP-712](./eip-712.md) typed data message proving it controls a subscriber address, without revealing that address in advance. **[EIP-712](./eip-712.md) Domain:** The domain's `chainId` and `verifyingContract` correspond to the registry the client subscribed on. This binds the signature to a specific chain and registry, preventing cross-chain replay. ```json { "name": "ERC-8162: Agent Subscription Protocol", "version": "1", "chainId": 8453, "verifyingContract": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18" } ``` **[EIP-712](./eip-712.md) Type:** ```solidity struct SubscriptionProof { uint256 agentId; // the agent being accessed bytes challenge; // opaque challenge from the agent (empty if none) } ``` - `agentId` binds the proof to a specific agent, preventing a signature meant for agent A from being replayed on agent B. - `challenge` is the opaque value from the `SUBSCRIPTION-REQUIRED` response. If the agent did not include a `challenge`, the client signs with empty bytes (`0x`). - The [EIP-712](./eip-712.md) domain's `chainId` and `verifyingContract` implicitly bind the proof to the specific registry the client subscribed on. The client signs this with their wallet private key. Neither the subscriber address nor the subscription ID are included; the agent recovers the address via `ecrecover` and looks up the subscription onchain. The `challenge` mechanism gives agents full control over their security model: | Use case | Challenge value | Agent validation | |----------|----------------|-----------------| | No replay protection needed | Omitted (client signs `0x`) | None | | Nonce-based replay protection | Random nonce | Agent tracks used nonces | | Time-bound access | Timestamp or expiry | Agent checks freshness | | Session binding | Session ID | Agent validates session | | Request-specific binding | `keccak256(method + path + body)` | Agent recomputes and compares | #### Step 4: Client Retries with SUBSCRIPTION-SIGNATURE The client re-sends the original request with a `SUBSCRIPTION-SIGNATURE` header containing a base64-encoded JSON object: ```http GET /api/chat HTTP/1.1 Host: agent.example.com Content-Type: application/json SUBSCRIPTION-SIGNATURE: eyJzdWJzY3JpcHRpb25JZ... ``` Decoded `SUBSCRIPTION-SIGNATURE` payload: ```json { "authorization": { "agentId": 42, "registryChain": "eip155:8453", "registryAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "challenge": "0x1a2b3c4d..." }, "signature": "0x2d6a8f..." } ``` | Field | Required | Description | |-------|----------|-------------| | `authorization` | MUST | The subscription authorization object, containing the data that was signed. | | `authorization.agentId` | MUST | The [ERC-8004](./eip-8004.md) agent identifier on the chosen chain. MUST match the `agentId` from the corresponding `registries` entry in the `SUBSCRIPTION-REQUIRED` response. | | `authorization.registryChain` | MUST | CAIP-2 chain identifier where the client's subscription exists. MUST match the `chain` from the corresponding `registries` entry. | | `authorization.registryAddress` | MUST | Address of the SubscriptionRegistry on that chain. MUST match the `address` from the corresponding `registries` entry. | | `authorization.challenge` | MUST | The challenge from `SUBSCRIPTION-REQUIRED`, or `"0x"` if none was provided. | | `signature` | MUST | [EIP-712](./eip-712.md) signature over the `SubscriptionProof` struct (derived from the `authorization` fields). | The subscriber address is NOT included; it is recovered from the signature via `ecrecover`. The subscription ID is NOT included; the agent looks it up via `verifyAccess(recoveredAddress, agentId, planId)`. The client specifies which chain and registry to verify against, allowing agents registered on multiple chains to route verification correctly. #### Step 5: Agent Verifies and Serves The agent performs the following verification: 1. **Validate registry**: Verify that the `registryChain`, `registryAddress`, and `agentId` from the `SUBSCRIPTION-SIGNATURE` match one of the agent's known registry entries. Reject unknown combinations immediately. 2. **Recover signer**: Use `ecrecover` on the [EIP-712](./eip-712.md) signature (using the domain corresponding to the specified chain and registry) to obtain the subscriber address. 3. **Validate challenge** (if one was issued): The agent validates the challenge according to its own logic (e.g., check nonce has not been used, verify timestamp is fresh, confirm session ID). If no challenge was issued, this step is skipped. 4. **Verify access**: Call `verifyAccess(recoveredAddress, agentId, planId)` on the SubscriptionRegistry at the specified chain and address, where `planId` is `0` to accept any plan or a specific plan ID for tier-based access control. The agent MAY also check a local cache synced from onchain events. 5. **Serve resource**: If all checks pass, return the requested resource with HTTP 200. If verification fails, the agent SHOULD respond with HTTP 403 (Forbidden) and an appropriate error message indicating whether the subscription is expired or the signature is invalid. #### Caching Recommendation To avoid an RPC call per request, agents SHOULD maintain a local cache of subscription state by indexing `Subscribed` and `Renewed` events. The agent re-syncs with the chain on a cadence appropriate for its use case (e.g., every block, every minute). Agents with low request volume MAY use onchain verification directly. ### x402 Interoperability This ERC and x402 are independent protocols that an agent can support simultaneously. The 402 access flow described here is solely for subscription-based access. An agent that also supports x402 pay-per-request handles x402 through its own 402 response and header exchange, separate from the `SUBSCRIPTION-REQUIRED` / `SUBSCRIPTION-SIGNATURE` flow. The two protocols do not share headers or payload formats. The agent decides which to use based on endpoint configuration or the headers present on incoming requests. | Aspect | x402 | [ERC-8162](./eip-8162.md) | |--------|------|----------| | **402 means** | "Pay me" | "Identify yourself" | | **Client authorizes** | A token transfer | Subscription identity lookup | | **Authorization type** | Payment authorization ([EIP-3009](./eip-3009.md) / Permit2) | Subscription authorization ([EIP-712](./eip-712.md)) | | **Onchain effect per request** | Yes, token transfer settles | No, read-only verification | | **Cost per request** | Token amount specified by agent | Zero (pre-paid via subscription) | | **Payment timing** | At request time | Before request time (subscription) | | **Third party needed** | Facilitator (verify + settle) | None (agent verifies directly) | | **Replay protection** | Built-in (onchain nonce consumed) | Agent's choice via `challenge` | | **Best for** | Occasional or unpredictable usage | Regular, high-frequency usage | ### Privacy Considerations Subscription state is stored onchain, but the HTTP 402 access flow ensures that subscriber identity is never exposed to the network layer unless a valid subscription exists. | Data | Visibility | |------|-----------| | Subscriber address | Onchain, public (in subscription record) | | Agent subscribed to | Onchain, public | | Plan tier | Onchain, public | | Amount paid | Onchain, public | | Duration | Onchain, public | | Subscriber identity at request time | Recoverable from the `SUBSCRIPTION-SIGNATURE` header by any observer via `ecrecover` | | Service usage / API calls | Offchain, private | | Request/response content | Offchain, private | The initial request (Step 1) carries no identifying information. The subscriber address is only exposed in the retry (Step 4), after the agent has responded with a 402 challenge. However, any party that can observe the `SUBSCRIPTION-SIGNATURE` header can recover the subscriber address from the [EIP-712](./eip-712.md) signature using the public domain parameters. The header does not provide confidentiality. Onchain, subscription data is public. This is comparable to what is visible in any [ERC-20](./eip-20.md) transfer and is acceptable for most use cases. As always, use of TLS is RECOMMENDED to protect headers from network observers. ## Rationale ### Why direct payment? Tokens are transferred directly to the agent owner on every `subscribe` and `renew` call. The registry contract never holds funds. - **Simplicity**: The contract only needs to track subscription state (start time, end time) and facilitate token transfers. The state footprint is minimal. - **Immediate revenue**: Agent owners receive payment at the moment of subscription or renewal. - **Minimal trust surface**: The registry facilitates a direct transfer between subscriber and agent owner. It never custodies funds, reducing the contract's attack surface. Subscriptions are prepaid: the subscriber pays upfront and receives access for the full period. Without auto-renewal, a cancel would only prevent the subscriber from voluntarily extending their own subscription. This provides no benefit and would be a strict downgrade. Subscriptions simply expire at `endTime`, matching user expectations from prepaid services. ### Why cycle-based pricing? Real-world subscriptions are cycle-based: Netflix charges per month, Spotify charges per month, GitHub charges per month or per year. Users understand and expect this model. - **Intuitive**: "5 USDC per month" is immediately clear. Users see the price and the period, with nothing to calculate. - **Simple math**: Total cost = `price * cycles`. - **Predictable**: Both parties know the exact amount charged and the exact duration of service. - **Natural alignment**: Billing cycles naturally align with service delivery periods, making plan management straightforward. ### Why onchain plans? Plans could be defined purely offchain (as [ERC-8004](./eip-8004.md) does with agent metadata), with only a hash stored onchain. However, onchain plans enable: - Trustless price verification: users know exactly what they are paying. - Smart contract composability: other contracts can read plan details. - Transparent pricing: anyone can verify the plan terms. ### Why hash-based subscription IDs? Sequential IDs leak information: subscriber count, growth rate, and ordering. Hash-based IDs (`keccak256(subscriber, agentId, planId, nonce)`) prevent casual enumeration while remaining deterministic and verifiable. ## Backwards Compatibility This ERC introduces no backwards compatibility issues. It is a new contract that references but does not modify existing standards ([ERC-8004](./eip-8004.md), [ERC-20](./eip-20.md), [EIP-2612](./eip-2612.md)). ## Test Cases ### Plan Creation | Test | Input | Expected | |------|-------|----------| | Create valid plan | `agentId=42, planId=1, asset=USDC, price=5000000, cycleDuration=2592000` | `PlanCreated` event emitted; `getPlan` returns correct values | | Non-owner creates plan | Caller is not `ownerOf(agentId)` | Reverts | | Duplicate planId | Same `(agentId, planId)` pair already exists | Reverts | | Zero price | `price=0` | Reverts | | Zero cycle duration | `cycleDuration=0` | Reverts | ### Subscribe | Test | Input | Expected | |------|-------|----------| | Subscribe 1 cycle | `agentId=42, planId=1, cycles=1` | `Subscribed` event; `endTime = startTime + cycleDuration`; 5 USDC transferred to agent owner | | Subscribe 3 cycles | `agentId=42, planId=1, cycles=3` | `endTime = startTime + 3 * cycleDuration`; 15 USDC transferred | | Subscribe to inactive plan | Plan has `active=false` | Reverts | | Duplicate active subscription | Already has active sub for same `(agentId, planId)` | Reverts | | Insufficient token approval | Approval < `price * cycles` | Reverts | ### Renew | Test | Input | Expected | |------|-------|----------| | Renew active subscription | 1 cycle renewal while active | `endTime` extended by `cycleDuration`; tokens transferred at current plan price | | Renew expired subscription | Plan is active, subscription expired | `startTime` resets to now; `endTime = now + cycleDuration` | | Renew expired, plan inactive | Plan `active=false`, subscription expired | Reverts | ### Access Verification | Test | Input | Expected | |------|-------|----------| | Active subscription | `block.timestamp` between `startTime` and `endTime` | `isActive` returns true; `verifyAccess` returns true | | Expired subscription | `block.timestamp > endTime` | `isActive` returns false; `verifyAccess` returns false | | Any-plan check | `planId=0`, subscriber has active sub on planId=2 | `verifyAccess` returns true | ## Reference Implementation See `ISubscriptionRegistry.sol` and `SubscriptionRegistry.sol` in the reference repository. ## Security Considerations ### Reentrancy The registry MUST use reentrancy guards on all state-mutating functions that interact with external contracts (token transfers). The `subscribe`, `renew`, and any permit-based variants call `safeTransferFrom` on an external [ERC-20](./eip-20.md) token, which could trigger a callback if the token is malicious. ### Front-running Subscription creation is not sensitive to front-running because `subscriptionId` is derived from the subscriber's address. An attacker cannot subscribe on behalf of another user without their token approval. ### Price Changes and Renewals Plan prices are NOT snapshotted into the subscription. Renewals always use the plan's current `price`. Agent owners can update plan pricing at any time, and the new price takes effect on the next renewal. Subscribers should be aware that renewing an expired subscription charges the current plan price. Active renewals (extending from `endTime`) also use the current price. This is the standard behavior for real-world subscription services. ### Token Compatibility The registry SHOULD validate that the payment token implements [ERC-20](./eip-20.md). Rebasing tokens and fee-on-transfer tokens are NOT supported and MAY cause accounting errors. ### Integer Overflow `price` uses `uint256` and `cycles` uses `uint32`, so `price * cycles` can overflow if `price` is extremely large. Implementations MUST use safe arithmetic or Solidity 0.8+ built-in overflow checks. ### Signature Replay (HTTP 402 Flow) The minimal `SubscriptionProof` (just `agentId` + empty `challenge`) is intentionally replayable. It proves wallet ownership, not request uniqueness. Agents that require replay protection MUST issue a `challenge` (e.g., a random nonce) in the `SUBSCRIPTION-REQUIRED` response and validate it on receipt. The [EIP-712](./eip-712.md) domain separator binds all signatures to a specific chain and registry address, preventing cross-chain replay. ### Challenge-less Mode Trade-offs When no challenge is issued, an intercepted `SUBSCRIPTION-SIGNATURE` can be reused by an attacker to access the same agent until the subscription expires. This is acceptable for low-sensitivity endpoints (similar to a bearer token). Agents serving sensitive data SHOULD always issue a challenge. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).