EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8162: Agent Subscription

An interface for managing subscriptions to onchain AI agent services with cycle-based pricing and direct payment.

Authors
Created2026-02-16
Discussion Linkhttps://ethereum-magicians.org/t/erc-8162-agent-subscription-protocol/27751
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1545PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationOverviewPricing ModelDirect Payment ModelData StructuresPlanSubscriptionEventsConfigurationPlan ManagementSubscription LifecycleSubscribeRenewAccess VerificationSame-Chain Identity BindingHash-Based Subscription IDsImmutable Subscription TermsAgent Card ExtensionHTTP 402 Access FlowFlow OverviewStep 1: Initial RequestStep 2: Agent Responds with 402 and SUBSCRIPTION-REQUIREDStep 3: Client Signs EIP-712 ProofStep 4: Client Retries with SUBSCRIPTION-SIGNATUREStep 5: Agent Verifies and ServesCaching Recommendationx402 InteroperabilityPrivacy ConsiderationsRationaleWhy direct payment?Why cycle-based pricing?Why onchain plans?Why hash-based subscription IDs?Backwards CompatibilityTest CasesPlan CreationSubscribeRenewAccess VerificationReference ImplementationSecurity ConsiderationsReentrancyFront-runningPrice Changes and RenewalsToken CompatibilityInteger OverflowSignature Replay (HTTP 402 Flow)Challenge-less Mode Trade-offsCopyright

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, with payments in ERC-20 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 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) 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 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 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 Identity Registry and uses Arbitrum's SubscriptionRegistry.

The registry interacts with:

  • An ERC-8004 Identity Registry (same chain) for agent ownership verification.
  • ERC-20 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

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

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:

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 agent NFT for the given agentId.

To create a plan, the agent owner calls:

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 token; fee-on-transfer and rebasing tokens are NOT supported. A PlanCreated event MUST be emitted.

To update pricing on an existing plan:

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:

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:

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:

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 approval and subscription in a single transaction:

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 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:

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:

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:

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):

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:

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 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 agent registration file (resolved via agentURI). The following extension to the agent card schema is RECOMMENDED:

{
  "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 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.

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/1.1 402 Payment Required
SUBSCRIPTION-REQUIRED: eyJ0eXBlIjoic3Vic2Ny...

Decoded SUBSCRIPTION-REQUIRED payload:

{
  "type": "subscription",
  "registries": [
    {
      "chain": "eip155:8453",
      "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
      "agentId": 42
    }
  ],
  "challenge": "0x1a2b3c4d..."
}
FieldRequiredDescription
typeMUSTAlways "subscription".
registriesMUSTArray 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 agent identifier on that chain's identity registry. The agentId MAY differ across chains.
challengeMAYOpaque 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 Proof

The client constructs and signs an EIP-712 typed data message proving it controls a subscriber address, without revealing that address in advance.

EIP-712 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.

{
  "name": "ERC-8162: Agent Subscription Protocol",
  "version": "1",
  "chainId": 8453,
  "verifyingContract": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
}

EIP-712 Type:

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 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 caseChallenge valueAgent validation
No replay protection neededOmitted (client signs 0x)None
Nonce-based replay protectionRandom nonceAgent tracks used nonces
Time-bound accessTimestamp or expiryAgent checks freshness
Session bindingSession IDAgent validates session
Request-specific bindingkeccak256(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:

GET /api/chat HTTP/1.1
Host: agent.example.com
Content-Type: application/json
SUBSCRIPTION-SIGNATURE: eyJzdWJzY3JpcHRpb25JZ...

Decoded SUBSCRIPTION-SIGNATURE payload:

{
  "authorization": {
    "agentId": 42,
    "registryChain": "eip155:8453",
    "registryAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
    "challenge": "0x1a2b3c4d..."
  },
  "signature": "0x2d6a8f..."
}
FieldRequiredDescription
authorizationMUSTThe subscription authorization object, containing the data that was signed.
authorization.agentIdMUSTThe ERC-8004 agent identifier on the chosen chain. MUST match the agentId from the corresponding registries entry in the SUBSCRIPTION-REQUIRED response.
authorization.registryChainMUSTCAIP-2 chain identifier where the client's subscription exists. MUST match the chain from the corresponding registries entry.
authorization.registryAddressMUSTAddress of the SubscriptionRegistry on that chain. MUST match the address from the corresponding registries entry.
authorization.challengeMUSTThe challenge from SUBSCRIPTION-REQUIRED, or "0x" if none was provided.
signatureMUSTEIP-712 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 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.

Aspectx402ERC-8162
402 means"Pay me""Identify yourself"
Client authorizesA token transferSubscription identity lookup
Authorization typePayment authorization (EIP-3009 / Permit2)Subscription authorization (EIP-712)
Onchain effect per requestYes, token transfer settlesNo, read-only verification
Cost per requestToken amount specified by agentZero (pre-paid via subscription)
Payment timingAt request timeBefore request time (subscription)
Third party neededFacilitator (verify + settle)None (agent verifies directly)
Replay protectionBuilt-in (onchain nonce consumed)Agent's choice via challenge
Best forOccasional or unpredictable usageRegular, 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.

DataVisibility
Subscriber addressOnchain, public (in subscription record)
Agent subscribed toOnchain, public
Plan tierOnchain, public
Amount paidOnchain, public
DurationOnchain, public
Subscriber identity at request timeRecoverable from the SUBSCRIPTION-SIGNATURE header by any observer via ecrecover
Service usage / API callsOffchain, private
Request/response contentOffchain, 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 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 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 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, ERC-20, EIP-2612).

Test Cases

Plan Creation

TestInputExpected
Create valid planagentId=42, planId=1, asset=USDC, price=5000000, cycleDuration=2592000PlanCreated event emitted; getPlan returns correct values
Non-owner creates planCaller is not ownerOf(agentId)Reverts
Duplicate planIdSame (agentId, planId) pair already existsReverts
Zero priceprice=0Reverts
Zero cycle durationcycleDuration=0Reverts

Subscribe

TestInputExpected
Subscribe 1 cycleagentId=42, planId=1, cycles=1Subscribed event; endTime = startTime + cycleDuration; 5 USDC transferred to agent owner
Subscribe 3 cyclesagentId=42, planId=1, cycles=3endTime = startTime + 3 * cycleDuration; 15 USDC transferred
Subscribe to inactive planPlan has active=falseReverts
Duplicate active subscriptionAlready has active sub for same (agentId, planId)Reverts
Insufficient token approvalApproval < price * cyclesReverts

Renew

TestInputExpected
Renew active subscription1 cycle renewal while activeendTime extended by cycleDuration; tokens transferred at current plan price
Renew expired subscriptionPlan is active, subscription expiredstartTime resets to now; endTime = now + cycleDuration
Renew expired, plan inactivePlan active=false, subscription expiredReverts

Access Verification

TestInputExpected
Active subscriptionblock.timestamp between startTime and endTimeisActive returns true; verifyAccess returns true
Expired subscriptionblock.timestamp > endTimeisActive returns false; verifyAccess returns false
Any-plan checkplanId=0, subscriber has active sub on planId=2verifyAccess 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 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. 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 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 and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth