StrimzStrimz
Guides

Build a subscription

Recurring stablecoin billing. The customer signs once at checkout and the scheduler charges each period.

Strimz subscriptions are pull-based. Your customer signs once at checkout to let the contract charge a fixed amount on a fixed schedule. After that, the Strimz scheduler takes each charge when it is due, with no further prompts.

Define a plan

const plan = await strimz.subscriptionPlans.create({
  name: 'Pro',
  amount: '20000000', // 20 USDC, 6 decimals
  currency: 'USDC',
  interval: 'monthly',
  intervalCount: 1,
  trialPeriodDays: 14,
})
 
const checkoutUrl = `https://checkout.strimz.finance/sub/${plan.id}`

Plans are templates. They don't represent a single customer; they represent a recurring price the customer can subscribe to. The plan object has no checkout URL field, so build the link from plan.id.

Customer subscribes

Send the customer to the checkout link. They connect a wallet, see "Subscribe, 20 USDC/month", and sign two typed-data messages:

  1. An EIP-2612 Permit that gives the StrimzSubscriptions contract an allowance on their USDC.
  2. A Strimz SubscriptionIntent that binds the plan, amount, interval and start time they agreed to.

Strimz's relayer submits both in one permitAndCreateSubscription transaction. That transaction creates the subscription and moves no money.

The full security model lives on the meta-tx flow page: what the customer signs, why the relayer cannot move funds on its own, when the signature expires.

You receive subscription.created once the indexer sees the enrolment, and subscription.charged each time a period charges.

What happens at charge time

The Strimz scheduler runs a sweep every 15 minutes. It finds subscriptions whose nextChargeAt (and any retry time) has passed, locks each one with chargeLock, and enqueues one job per subscription onto strimz.subscription.due.

The worker calls batchCharge([subscriptionId], [chargeAttemptId]) for that one subscription. The attempt id is keccak256(abi.encodePacked(onchainSubId, currentPeriodEndAt, attempt)), so a replayed attempt can't charge twice: the contract skips it with outcome Duplicate. The contract pulls USDC from the customer's wallet and emits SubscriptionCharged, or SubscriptionChargeSkipped with the reason. The indexer projects both. See How charges fire.

Handle a failed charge

A payment failure moves the subscription to at_risk and fires subscription.charge_failed. The payload is { subscription, charge }, and charge.outcome is one of:

outcomeWhat it means
insufficient_fundsCustomer's balance is below the charge amount
revoked_approvalCustomer's allowance to the contract is too low
transfer_failedThe token transfer failed, for example a blocklisted wallet
merchant_inactiveYour merchant account is not active in the registry

Other outcomes, such as not_due or duplicate, are recorded but don't fire a webhook.

The scheduler retries after 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours, then every 24 hours. If the charge still hasn't succeeded 48 hours after the period end, an hourly cron flips the subscription to lapsed and fires subscription.lapsed. If you've enabled the agent's recovery capability, the customer also gets emails. See Recovery.

Cancel a subscription

There are two paths to cancellation. Both are immediate.

From your server. Say the customer's account was deleted on your side:

await strimz.subscriptions.cancel({ id: subscriptionId, reason: 'merchant initiated' })

The row becomes cancelled straight away, subscription.cancelled fires during the call, and the scheduler stops charging. The API also queues an on-chain cancel, which works from the next testnet redeploy (#142) and reverts on the current deployment.

From the customer's wallet. The customer calls cancel() on the contract directly. The indexer picks up SubscriptionCancelled and flips the row. No webhook fires for this path today.

Either way, the customer stops getting charged. Their token allowance still exists. To remove it, the customer calls approve(spender, 0) on the USDC contract.

Trial periods

trialPeriodDays: 14 on the plan means the first charge is due 14 days after enrolment instead of at enrolment. The customer signs immediately; Strimz takes nothing until the trial ends. A payer gets the trial only on their first subscription to that plan.

Without a trial, the first charge is due at enrolment and the scheduler takes it on its next sweep, within about 15 minutes.

Pricing math

Strimz charges a per-transaction fee on every charge based on your tier. For a Starter (0.5%) merchant subscribing a customer to a 20 USDC monthly plan:

  • Customer pays: 20.00 USDC
  • Strimz fee: 0.10 USDC (0.5%)
  • Merchant receives: 19.90 USDC

The fee is taken on chain, in the same transaction as the charge, by StrimzSubscriptions. There's no separate invoicing. Your dashboard's MRR view shows both gross and net.

On this page