Skip to main content

What is a Plan?

A plan is the billing template that a merchant publishes on-chain. It defines what subscribers are paying for, how much, how often, and under what terms. Subscribers choose a plan and create a subscription against it. Plans are shared objects - one plan can have thousands of active subscriptions. The plan itself stores no per-subscriber state.

Plan Fields

FieldTypeDescription
idu64Auto-incremented unique identifier assigned at creation
merchantAddressThe Stellar address that receives subscription payments
tokenAddressThe SAC token contract used for billing (e.g., USDC)
amounti128Amount charged per billing period (in token’s smallest unit, e.g., stroops)
periodu64Billing period length in seconds (e.g., 2592000 for ~30 days)
trial_periodsu32Number of free periods before billing begins (0 = no trial)
max_periodsu32Maximum number of billing periods before auto-expiry (0 = unlimited)
grace_periodu64Time in seconds after a failed charge before the subscription pauses
price_ceilingi128Maximum amount the merchant can ever set via update_plan_amount
created_atu64Ledger timestamp when the plan was created
activeboolWhether the plan accepts new subscriptions (true by default)

Creating a Plan

1

Merchant calls create_plan

The merchant invokes create_plan with all plan parameters. The contract calls merchant.require_auth() to verify the caller owns the merchant address.
2

Validation checks

The contract validates the input:
  • amount must be greater than 0
  • period must be greater than 0
  • price_ceiling must be greater than or equal to amount
If any check fails, the transaction reverts with a descriptive error.
3

Plan is stored

The contract increments NextPlanId, constructs the Plan struct, and writes it to persistent storage under DataKey::Plan(id). The plan ID is also appended to the merchant’s index at DataKey::MerchantPlans(merchant).
4

Event emitted

A PlanCreated event is emitted with the full plan struct, making it indexable by off-chain systems.
import { VowenaClient } from "vowena";

const client = new VowenaClient({ contractId, networkPassphrase, rpcUrl });

const tx = await client.createPlan({
  merchant: merchantKeypair.publicKey(),
  token: USDC_CONTRACT_ID,
  amount: 10_0000000n,        // 10 USDC (7 decimals)
  period: 2592000n,           // ~30 days
  trial_periods: 1,           // 1 free period
  max_periods: 12,            // 12 months max
  grace_period: 259200n,      // 3 days grace
  price_ceiling: 15_0000000n, // Can raise up to 15 USDC
});

Price Ceiling

The price ceiling is one of Vowena’s core consumer protection features. When a subscriber signs up, they authorize a token allowance based on the ceiling, not the current amount.
The price ceiling is immutable after plan creation. A merchant cannot raise the ceiling - ever. This is by design: it guarantees that subscribers know the absolute maximum they could ever be charged per period.

How it works

Merchant flexibility

The merchant can call update_plan_amount to adjust the price up or down, as long as the new amount stays at or below the ceiling. This allows for small price adjustments, seasonal promotions, or inflation adjustments without requiring subscribers to re-authorize.

Subscriber guarantee

The subscriber’s token allowance is calculated from price_ceiling * periods, not amount * periods. Even if the merchant raises the price to the ceiling, the allowance already covers it. The subscriber never needs to sign again.
Set your price ceiling thoughtfully. Too low and you cannot adjust prices. Too high and subscribers may hesitate to approve a large allowance. A ceiling of 1.5x to 2x your initial price is a reasonable default.

Example

ScenarioAmountCeilingAllowed?
Initial plan10 USDC15 USDCYes
Small raise12 USDC15 USDCYes - within ceiling
Promotion8 USDC15 USDCYes - can lower freely
Big raise20 USDC15 USDCNo - exceeds ceiling, use migration

Plan Immutability

Plans are intentionally immutable once created. The only mutable field is amount (within the ceiling). You cannot change:
  • The billing period
  • The token
  • The merchant address
  • Trial periods or max periods
  • The grace period
  • The price ceiling itself
If you need to change any immutable field, create a new plan and use the migration system to move subscribers over with their explicit consent.

Deactivating a Plan

Merchants can deactivate a plan to stop new subscriptions. Existing subscriptions continue to bill normally - deactivation only prevents new subscribe() calls.
const tx = await client.deactivatePlan({
  merchant: merchantKeypair.publicKey(),
  plan_id: 1n,
});
Deactivation is a soft state change. The plan data remains on-chain and existing subscribers are unaffected. There is no “delete plan” - on-chain data is permanent.

What’s Next

Subscriptions

See how subscribers activate plans and the full subscription lifecycle.

Billing

Understand what happens when a charge is triggered.