Strimz
Subscriptions

Plans

A SubscriptionPlan is the template. Name, amount, interval, optional trial. One plan, many subscribers. Plans are immutable after creation; that's an intentional choice.

A plan is the thing your customers subscribe to. "Pro Monthly," "Team Annual," "Starter. 5 USDC/month." You create the plan once with subscriptionPlans.create; payers subscribe by signing an EIP-2612 permit on the plan's hosted-checkout URL; each one becomes a Subscription row that points back at the plan.

Creating one

const plan = await strimz.subscriptionPlans.create({
  name: 'Pro, Monthly',
  description: 'All features, unlimited seats, monthly billing.',
  amount: '9990000',       // 9.99 USDC, 6-decimal raw
  currency: 'USDC',
  interval: 'monthly',
  intervalCount: 1,
  trialPeriodDays: 14,
})

What each field does:

name shows up everywhere. On the hosted subscription checkout page, on receipt emails, on the dashboard's plan list, in the merchant-notification email when someone subscribes. Make it specific. "Pro. Monthly" is better than "Pro" if you have a yearly version too. Maximum 120 chars.

description is optional but useful. Goes under the name on hosted checkout. Maximum 500 chars; we don't render Markdown so plain text.

amount is a 6-decimal raw integer string. '9990000' is 9.99 USDC. (USDC and EURC are both 6-decimal; if a future currency ships with different decimals, we'd revisit but that's not on the immediate roadmap.)

currency is 'USDC' or 'EURC'. We don't currently support yield-bearing stablecoins for subscriptions because most of them don't ship EIP-2612 permit, which we need for the no-extra-signature recurring flow. If that changes upstream, we'll add support.

interval is 'daily', 'weekly', 'monthly', or 'yearly'. intervalCount multiplies it. intervalCount: 2 with interval: 'monthly' is every two months. Default intervalCount is 1.

trialPeriodDays is the free-trial length. The payer signs the permit on day zero; we don't charge until trialPeriodDays later. Null or omitted means no trial. The first charge fires in the same transaction as the enrolment.

Plans are immutable

You can't change a plan's amount, interval, name, or anything else after creation. This isn't a limitation; it's an intentional design choice.

The reason: existing subscribers signed an EIP-2612 permit against the plan's terms at the time they subscribed. If we let you mutate the plan, we'd either have to silently override their consent or maintain version history that nobody would actually look at.

The pattern when you want to "change a plan" is to archive the existing plan and create a new one with the new terms. Existing subscribers stay on the old plan and the old price until they re-subscribe; new subscribers get the new plan. This matches what your customers expect: they signed up for "Pro at $10/month," that's what they pay until they explicitly choose to switch.

If you genuinely need to give one specific customer a different price (B2B negotiations, custom enterprise deals), the recommendation is one of:

  • Create a one-off plan with their negotiated price. Ugly in the dashboard but works.
  • Run the standard plan and issue periodic refunds for the difference.
  • Use invoices for the enterprise account. One-off pricing without the subscription scaffolding.

The third one is what most B2B SaaS does for enterprise; subscriptions are for the SMB self-serve flow.

Listing plans

const page = await strimz.subscriptionPlans.list({
  status: 'active',
  limit: 50,
})

Returns a paginated Page<SubscriptionPlan>. nextCursor is what you pass on the next call to advance.

If you have a small number of plans (under a hundred is usual for B2B SaaS), one call with limit: 100 covers you. Stripe taught us that almost nobody has so many plans that they need to think about pagination.

Archiving

await strimz.subscriptionPlans.archive('plan_…')

Archive stops new subscriptions on the plan. The hosted-checkout URL for the plan starts returning 410 Gone; the API refuses new subscriptions.create against the plan. Existing subscribers keep cycling on the old terms because, again, we honour their original consent.

To stop both new and existing, archive the plan and then iterate subscriptions.list({ planId, status: 'active' }) and cancel each one. There's no bulk-cancel call; if you have thousands of subscribers on a plan you want to kill, you'll write a script. We can probably add a bulk-cancel later; nobody's asked for one yet.

Each plan has a hosted-checkout URL:

https://strimz-finance.vercel.app/sub/<planId>

You send this to anyone you want to subscribe. The page handles wallet connect, the EIP-2612 permit signature, and a Strimz SubscriptionIntent signature that binds the plan the payer is enrolling in. Two typed-data prompts in one checkout flow. The first cycle charge lands in the same on-chain transaction as the enrolment.

You don't need to call subscriptions.create from the API for this flow. The hosted checkout flow handles it end-to-end. Payer signs, our relayer broadcasts permitAndCreateSubscription(...), the indexer projects the resulting Subscription, and your subscription.created webhook fires.

If you want to embed the enrolment in your own UI, use useSubscriptionCheckout(planId) from the React SDK. See Embedded checkout.

The plan ↔ subscription relationship

The plan is the template; each Subscription is an instance. When a payer subscribes, we snapshot the plan's amount, currency, interval, and intervalCount onto the subscription row. The relationship is:

SubscriptionPlan
  ├─ id, name, amount, currency, interval, intervalCount, trialPeriodDays

Subscription
  ├─ id, planId (references the plan)
  ├─ payerAddress
  ├─ amount, currency, interval, intervalCount  (snapshotted from the plan at create time)
  ├─ currentPeriodStartAt, currentPeriodEndAt
  ├─ status (trialing | active | at_risk | paused | cancelled | lapsed)

We snapshot the pricing onto the subscription because it makes the contract simpler , batchCharge reads the amount from the subscription row, not from the plan. The plan and the subscription drift if you create new plans later; that drift is the whole point.

Pricing notes

Round to multiples of 0.01 USDC. Sub-cent pricing technically works ('9999900' is 9.9999 USDC) but reads weird on receipts and confuses fee calculations.

Use the yearly interval for annual plans. Don't fake it with intervalCount: 12 and interval: 'monthly' , receipts will say "every 12 months," wallets will show the timing as monthly, and customers will get confused. The yearly interval is its own thing for good reasons.

Trial-period free days are part of the plan, not part of the subscription. If you want a custom trial length for one specific customer, create a one-off plan for them. We don't support per-subscription trial overrides because the plan's trialPeriodDays is signed into the contract's understanding of when the first charge should fire.

On this page