Skip to main content

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.
This design means there is no single point of failure for billing. If the merchant’s server goes down, a keeper network can continue charging. If all keepers go down, the merchant can charge manually. The protocol does not care who submits the transaction.

Complete Charge Flow

Every call to charge(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.
1

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.
2

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.
3

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.
4

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.
5

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.
6

Handle trial periods

If periods_billed < plan.trial_periods, this is a trial period. The contract:
  • Increments periods_billed
  • Advances next_billing_time by one period
  • Emits ChargeBilled with amount 0
  • Returns true
No token transfer occurs during trials. The allowance is preserved for paid periods.
7

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.
8

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.
Why pre-check instead of just calling transfer_from?If transfer_from is called and it fails (insufficient balance or allowance), the token contract panics. A panic in Soroban reverts the entire transaction - including all state changes the Vowena contract made up to that point.This means the contract cannot record the failure. It cannot set failed_at. It cannot emit ChargeFailed. It cannot start the grace period. The transaction simply disappears as if it never happened.By pre-checking, the contract detects insufficient funds before calling transfer_from. It can then record the failure, start the grace clock, emit events, and return false - all within a successful transaction that persists to the ledger.
9

Insufficient funds path

If the pre-check finds insufficient balance or allowance:
  • Sets failed_at to the current timestamp (starts the grace clock)
  • Emits ChargeFailed event with the timestamp
  • Returns false
The transaction succeeds (is recorded on-chain) even though the charge failed. This is intentional - it creates an auditable record of the failure and starts the grace period countdown.
10

Successful charge

If the pre-check passes:
  1. Calls token.transfer_from(contract_address, subscriber, merchant, amount)
  2. Increments periods_billed
  3. Advances next_billing_time by one billing period
  4. Clears failed_at (resets to 0 - any previous grace clock is cancelled)
  5. Emits ChargeBilled with the amount and updated period count
  6. Returns true

Grace Period State Machine

The grace period creates a recovery window when a charge fails. Here is the complete flow:
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.
If the subscriber tops up and charge() is called again before grace expires:
  • Pre-check passes
  • transfer_from succeeds
  • failed_at is cleared back to 0
  • Subscription stays Active as if nothing happened
The grace period is completely reset.
If charge() is called after failed_at + grace_period:
  • Subscription transitions to Paused
  • SubscriptionPaused event is emitted
  • Returns false
The subscription is now paused but recoverable via reactivate().
If a full billing period passes while the subscription is Paused and charge() is called again:
  • Subscription transitions to Cancelled (terminal)
  • SubscriptionCancelled event is emitted
  • Returns false
The subscription is now permanently over.
Set grace_period generously. A grace period of 259200 (3 days) gives subscribers time to receive failure notifications, transfer funds, and recover - without losing their subscription.

Charge Return Values

ReturnMeaningState Change
trueCharge succeeded (or trial period advanced)periods_billed incremented, next_billing_time advanced
falseCharge could not be processedDepends 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

Because charge() 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.