The Most Important Function
charge() is the heart of the Vowena protocol. It is called every billing period for every active subscription, and its design drives most of the contract’s architecture decisions.
charge() is permissionless. It takes no authorization. There is no require_auth() call. Anyone - the merchant, a keeper bot, a random third party - can call charge(sub_id) and trigger billing. The protocol validates everything on-chain: only the merchant receives funds, only the correct amount is transferred, and only at the right time.Complete Charge Flow
Every call tocharge(sub_id) executes the following steps in order. If any step fails, the function either reverts the transaction or returns false - depending on the failure type.
Load subscription and plan
The contract reads the
Subscription from DataKey::Sub(sub_id) and the associated Plan from DataKey::Plan(plan_id). If either does not exist, the transaction reverts.Handle Paused-to-Cancelled transition
If the subscription is Paused and an entire billing period has elapsed since it was paused, the contract transitions it to Cancelled, emits
SubscriptionCancelled, and returns false.This is how paused subscriptions eventually terminate - they get one extra period to recover before being permanently cancelled.Check status is Active
If the subscription status is not
Active, the function returns false. Only active subscriptions can be charged. This catches Cancelled, Expired, and freshly-paused subscriptions.Check billing time
The contract verifies that the current ledger timestamp is at or after
next_billing_time. If the subscription is not yet due, the function returns false. You cannot charge early.Check max periods (expiry)
If the plan has
max_periods > 0 and periods_billed >= max_periods, the subscription is transitioned to Expired, a SubscriptionExpired event is emitted, and the function returns false.Handle trial periods
If
periods_billed < plan.trial_periods, this is a trial period. The contract:- Increments
periods_billed - Advances
next_billing_timeby one period - Emits
ChargeBilledwith amount0 - Returns
true
Check grace period expiry
If the subscription has a
failed_at timestamp (a previous charge failed) and the current time exceeds failed_at + grace_period, the subscription is transitioned to Paused. A SubscriptionPaused event is emitted and the function returns false.This means the subscriber had a grace window to top up and the window has closed.Pre-check balance and allowance
This is the most critical step in the entire contract.Before attempting the token transfer, the contract reads the subscriber’s token balance and the contract’s remaining allowance, and verifies both are sufficient for the charge amount.
Insufficient funds path
If the pre-check finds insufficient balance or allowance:
- Sets
failed_atto the current timestamp (starts the grace clock) - Emits
ChargeFailedevent with the timestamp - Returns
false
Successful charge
If the pre-check passes:
- Calls
token.transfer_from(contract_address, subscriber, merchant, amount) - Increments
periods_billed - Advances
next_billing_timeby one billing period - Clears
failed_at(resets to0- any previous grace clock is cancelled) - Emits
ChargeBilledwith the amount and updated period count - Returns
true
Grace Period State Machine
The grace period creates a recovery window when a charge fails. Here is the complete flow:First failed charge
First failed charge
charge() is called, pre-check finds insufficient funds. failed_at is set to the current timestamp. Subscription remains Active. Grace clock starts ticking.The subscriber now has grace_period seconds to top up their wallet.Successful charge during grace
Successful charge during grace
If the subscriber tops up and
charge() is called again before grace expires:- Pre-check passes
transfer_fromsucceedsfailed_atis cleared back to0- Subscription stays Active as if nothing happened
Grace period expires
Grace period expires
If
charge() is called after failed_at + grace_period:- Subscription transitions to Paused
SubscriptionPausedevent is emitted- Returns
false
reactivate().Paused for one more period
Paused for one more period
If a full billing period passes while the subscription is Paused and
charge() is called again:- Subscription transitions to Cancelled (terminal)
SubscriptionCancelledevent is emitted- Returns
false
Charge Return Values
| Return | Meaning | State Change |
|---|---|---|
true | Charge succeeded (or trial period advanced) | periods_billed incremented, next_billing_time advanced |
false | Charge could not be processed | Depends on reason: status may change to Paused, Cancelled, or Expired; or failed_at set |
A
false return is not a transaction failure. The transaction succeeds and is recorded on-chain. The false indicates that no token transfer occurred. Always check the emitted events to understand what happened.Permissionless Charge: Keeper Bots
Becausecharge() requires no authorization, it enables a keeper bot ecosystem:
Merchant bot
The merchant runs their own cron job or backend service that calls
charge() for each subscription when billing time arrives.Third-party keeper
An independent keeper service monitors all active subscriptions and calls
charge() on time. The Vowena SDK includes a ready-made keeper implementation.Manual fallback
In the worst case, anyone can call
charge() from a block explorer or CLI. The protocol enforces all rules regardless of who submits the transaction.Keepers pay the Stellar transaction fee (~$0.00001). The economic incentive for third-party keepers can come from merchant tips, protocol rewards, or bundled services. The protocol itself does not charge fees.
Summary: Why This Design?
No single point of failure
Permissionless charging means billing continues even if the merchant’s infrastructure goes offline.
Auditable failures
Pre-checking balance before transfer ensures that every failed charge is recorded on-chain with a timestamp, enabling grace periods and notifications.
Atomic state transitions
Every charge call either completes fully or records the failure. There is no partial state - no “payment sent but not recorded” scenario.
No custody risk
Funds move directly from subscriber to merchant via
transfer_from. The contract never holds or custodies any tokens.