Subscriptions
How agents and service providers handle recurring billing on-chain.
A Subscription is an on-chain record tying an agent wallet to a Plan. It gives the service provider permission to pull a set amount of USDC from the agent’s wallet every billing cycle, and the agent never has to sign each individual transaction.
Subscription structure
interface Subscription {
id: string; // On-chain subscription id
subscriber: Address; // Agent wallet
plan: string; // Id of the plan being subscribed to
status: SubscriptionStatus; // TRIAL | ACTIVE | PAUSED | CANCELLED
startedAt: number; // Unix timestamp
trialEndsAt?: number; // Unix timestamp, if applicable
nextBillingAt: number; // Unix timestamp of next collection
cycleCount: number; // Number of completed billing cycles
authorizedAmount: number; // Max pull per cycle (in token base units)
}
Lifecycle
subscribe()
|
v
TRIAL (if trialPeriodDays > 0)
|
| trial period ends
v
ACTIVE <---+
| |
| | billing cycle completes, collection succeeds
| |
| collection fails (insufficient funds)
v
PAUSED -->--+ (after retry window, if still failing)
|
| agent cancels or provider cancels
v
CANCELLED
TRIAL
When a plan carries a trial period, the subscription begins in TRIAL status. Nothing gets collected while the trial runs. Billing starts with the first cycle after the trial ends.
ACTIVE
An ACTIVE subscription bills itself at every renewal date. Either the provider’s system or HoodLayer’s automation calls hoodlayer.collect() to pull funds from the subscriber’s wallet, and the on-chain Subscription Authority checks the authorization before executing the transfer.
PAUSED
When a collection attempt fails (say the agent wallet doesn’t hold enough USDC), the subscription moves to PAUSED. HoodLayer keeps retrying over a configurable window (default: 3 attempts over 7 days). If every retry fails, the subscription stays paused and a subscription.payment_failed webhook fires.
Once funds are topped up, either the agent or the provider can unpause manually.
CANCELLED
Cancelling stops billing right away, and the cancellation gets written on-chain. Whatever time was already paid for in the current cycle stays accessible (enforcing any grace period is the provider’s job).
How automatic billing works
Smart contracts on Robinhood Chain can’t pull from a wallet unless that wallet has explicitly authorized it. HoodLayer handles this with its own open-source HoodLayerSubscriptions contract, deployed on Robinhood Chain.
When an agent runs agent.subscribe(), the SDK writes a subscription record on-chain and delegates a Subscription Authority to the provider for that exact (wallet, token, authorized_amount) combination. The authority is narrowly scoped: the provider can pull at most the authorized amount per billing cycle, nothing more.
Come renewal time, the provider (or HoodLayer’s automation) calls collect() on the contract. It checks the subscription record, confirms the authorization still holds, and runs the token transfer. No individual billing transaction ever needs the agent’s signature.
Creating a subscription
From the agent side:
const sub = await agent.subscribe({
planId: "0x7f3a...plan_id",
});
console.log(sub.id); // on-chain subscription id
console.log(sub.status); // "TRIAL" or "ACTIVE"
console.log(sub.nextBillingAt); // Unix timestamp
Verifying a subscription
On the provider side, checking whether an incoming request comes with an active subscription looks like this:
const isValid = await hoodlayer.verifySubscription({
subscriber: requestingWallet,
plan: plan.id,
});
if (!isValid) {
return res.status(403).json({ error: "No active subscription" });
}
In most setups the payment gate middleware does this check for you.
Cancellation
// The agent ends their own subscription
await agent.cancelSubscription({ subscriptionId: sub.id });
// The provider ends it (e.g. for policy violations)
await hoodlayer.cancelSubscription({ subscriptionId: sub.id });
A cancellation is an on-chain operation, final and publicly verifiable.
Viewing active subscriptions
// Agent side: every active subscription for this wallet
const subscriptions = await agent.listSubscriptions();
// Provider side: everyone subscribed to a plan
const subscribers = await hoodlayer.listSubscribers({ plan: plan.id });
Subscriptions also show up in the HoodLayer Dashboard, next to MRR trends, churn rate, and per-subscriber transaction history.