This document is the complete reference for the FlowPay Soroban smart contract. It covers every public function, its parameters, return values, auth requirements, and error conditions.
The core data structure stored per subscriber.
pub struct Subscription {
pub merchant: Address, // Stellar address of the payment recipient
pub amount: i128, // Amount per period, in stroops (1 XLM = 10_000_000)
pub interval: u64, // Seconds between charges
pub last_charged: u64, // Ledger UNIX timestamp of the last successful charge
pub active: bool, // false if the subscription has been cancelled
pub paused: bool, // true if the subscription is temporarily paused
pub token: Address, // SAC token address used for this subscription
}Internal storage keys. Not part of the public API but useful for understanding storage layout.
pub enum DataKey {
Subscription(Address), // persistent — one entry per subscriber
Token, // instance — the token contract address
GracePeriod, // instance — seconds allowed for charge window
MerchantWhitelist(Address), // persistent — true if merchant is whitelisted
WhitelistEnabled, // instance — true if whitelist is active
FeeCollector, // instance — fee collector address
FeeBps, // instance — protocol fee in basis points
ActiveCount, // instance — running total of active subscriptions
MerchantRevenue(Address), // persistent — cumulative revenue per merchant
DailyLimit(Address), // temporary — user-set daily pay_per_use cap
DailySpent(Address), // temporary — amount spent today via pay_per_use
}One-time contract setup. Must be called before any other function.
initialize(env: Env, token: Address)
Parameters
| Name | Type | Description |
|---|---|---|
token |
Address |
The Stellar Asset Contract (SAC) address of the token to use for payments |
Auth: None required.
Storage written: DataKey::Token in instance storage.
Errors
| Condition | Panic message |
|---|---|
| Called more than once | "already initialized" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- initialize \
--token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSCCreates or overwrites a subscription for the calling user.
subscribe(env: Env, user: Address, merchant: Address, amount: i128, interval: u64, token: Address, trial_period: Option<u64>, referrer: Option<Address>)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
merchant |
Address |
The payment recipient. |
amount |
i128 |
Stroops to transfer per period. Must be > 0. |
interval |
u64 |
Seconds between charges. Must be > 0. Common values: 86400 (1 day), 604800 (1 week), 2592000 (~30 days). |
token |
Address |
The SAC address of the token to use for this subscription. |
trial_period |
Option<u64> |
Optional seconds to delay the first charge. If set, last_charged is initialized to now + trial_period. |
referrer |
Option<Address> |
Optional address of the referrer who introduced this subscriber. |
Auth: user.require_auth() — the transaction must be signed by user.
Whitelist: If the merchant whitelist is enabled, the merchant address must have been previously added by an admin via add_merchant.
Storage written: DataKey::Subscription(user) in persistent storage. last_charged is set to the current ledger timestamp (or now + trial_period if provided). DataKey::Referral(user) in persistent storage if referrer is provided.
Events emitted
topic: ("subscribed", user)
data: (merchant, amount, interval)
topic: ("referred", user) if referrer is provided
data: referrer_address
Errors
| Condition | Panic message / error |
|---|---|
amount <= 0 |
"amount must be positive" |
interval == 0 |
"interval must be positive" |
| Merchant not whitelisted (if enabled) | MerchantNotWhitelisted |
Pre-condition: The user must have called approve() on the token contract granting the FlowPay contract an allowance of at least amount before subscribing.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- subscribe \
--user <USER_ADDRESS> \
--merchant <MERCHANT_ADDRESS> \
--amount 50000000 \
--interval 2592000Triggers a recurring charge for a subscriber. Permissionless — anyone can call this.
charge(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber to charge. |
Auth: None. This function is intentionally permissionless so keeper services can call it without holding user keys.
What it does:
- Loads the subscription for
user - Asserts
active == true - Asserts
now >= last_charged + interval - If a
grace_periodis set, assertsnow <= last_charged + interval + grace_period - If a protocol fee is set, splits
amountbetweenFeeCollectorandmerchant - Calls
transfer_from(contract, user, recipient, amount)on the token contract - Updates
last_charged = now
Events emitted
topic: ("charged", user)
data: (merchant, amount, timestamp)
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription is paused | "subscription is paused" |
| Interval has not elapsed | "interval not elapsed yet" |
| Grace period elapsed | "grace period elapsed" |
| Contract not initialized | "not initialized" |
| Insufficient allowance | Host error from token contract |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <KEEPER_KEY> \
--network testnet \
-- charge \
--user <USER_ADDRESS>Instantly transfers an arbitrary amount from the user to their subscribed merchant. No interval check. Useful for metered or usage-based billing.
pay_per_use(env: Env, user: Address, amount: i128)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The payer. Must match the transaction signer. |
amount |
i128 |
Stroops to transfer. Must be > 0. |
Auth: user.require_auth().
What it does:
- Loads the subscription for
user - Asserts
active == true - Calls
transfer_from(contract, user, merchant, amount)on the token contract
Note: pay_per_use does not update last_charged. It is independent of the recurring billing cycle.
Events emitted
topic: ("pay_per_use", user)
data: (merchant, amount)
Errors
| Condition | Panic message |
|---|---|
amount <= 0 |
"amount must be positive" |
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription is paused | "subscription is paused" |
| Insufficient allowance | Host error from token contract |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- pay_per_use \
--user <USER_ADDRESS> \
--amount 1000000Temporarily halts charges for a subscription. The subscription record is preserved and can be resumed at any time. Both charge() and pay_per_use() will panic while paused.
pause(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
Auth: user.require_auth().
Events emitted
topic: ("paused", user)
data: ()
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription already paused | "subscription is already paused" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- pause \
--user <USER_ADDRESS>Resumes a paused subscription, re-enabling charge() and pay_per_use().
resume(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
Auth: user.require_auth().
Events emitted
topic: ("resumed", user)
data: ()
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription is not paused | "subscription is not paused" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- resume \
--user <USER_ADDRESS>Deactivates a subscription. The subscription record remains in storage with active = false. No further charges can be made.
cancel(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
Auth: user.require_auth().
Events emitted
topic: ("cancelled", user)
data: ()
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- cancel \
--user <USER_ADDRESS>Read-only view function. Returns the subscription for a given user, or None if none exists.
get_subscription(env: Env, user: Address) -> Option<Subscription>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to look up. |
Auth: None.
Returns: Option<Subscription> — None if no subscription exists for this address.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_subscription \
--user <USER_ADDRESS>Read-only view function. Returns the Unix timestamp of the next scheduled charge for a user.
next_charge_at(env: Env, user: Address) -> Option<u64>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to look up. |
Auth: None.
Returns: Option<u64> — Returns None if:
- No subscription exists for the user
- The subscription is inactive (cancelled)
Returns Some(last_charged + interval) if the subscription is active.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- next_charge_at \
--user <USER_ADDRESS>Charges multiple subscribers in a single transaction. Individual failures do not abort the batch — every address is processed and its outcome is returned.
batch_charge(env: Env, users: Vec<Address>) -> Vec<ChargeResult>
Parameters
| Name | Type | Description |
|---|---|---|
users |
Vec<Address> |
List of subscriber addresses to attempt charging. |
Auth: None. Same permissionless model as charge().
Returns: Vec<ChargeResult> — one entry per input address, in order.
pub enum ChargeResult {
Charged, // funds transferred successfully
Skipped, // interval has not elapsed yet
NoSubscription, // no subscription found for this address
Inactive, // subscription is cancelled
Paused, // subscription is paused
GracePeriodElapsed, // charge window has closed
}Storage written: DataKey::Subscription(user) updated for each Charged result. DataKey::MerchantRevenue(merchant) incremented for each Charged result.
Events emitted: ("charged", user) for each successfully charged user.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <KEEPER_KEY> \
--network testnet \
-- batch_charge \
--users '["<USER_A>","<USER_B>","<USER_C>"]'Returns the current number of active subscriptions. Incremented by subscribe(), decremented by cancel().
get_active_count(env: Env) -> u64
Auth: None.
Returns: u64 — total active subscriptions.
Storage read: DataKey::ActiveCount in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_active_countReturns the cumulative amount charged to a merchant's subscribers across all charge() and pay_per_use() calls.
get_merchant_revenue(env: Env, merchant: Address) -> i128
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to query. |
Auth: None.
Returns: i128 — total stroops received by this merchant. Returns 0 if no charges have occurred.
Storage read: DataKey::MerchantRevenue(merchant) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_merchant_revenue \
--merchant <MERCHANT_ADDRESS>Sets a daily spending cap for pay_per_use() for the calling user. The limit is stored in temporary storage and resets automatically after approximately one day (~17,280 ledgers at 5 s/ledger).
set_daily_limit(env: Env, user: Address, limit: i128)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
limit |
i128 |
Maximum stroops spendable via pay_per_use() per day. Must be > 0. |
Auth: user.require_auth().
Storage written: DataKey::DailyLimit(user) in temporary storage with TTL of ~1 day.
Enforcement: Every pay_per_use() call checks DailySpent(user) + amount <= DailyLimit(user) before transferring. The running total is tracked in DataKey::DailySpent(user) (also temporary, same TTL).
Errors
| Condition | Panic message |
|---|---|
limit <= 0 |
"limit must be positive" |
| Spend would exceed limit | "daily spending limit exceeded" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- set_daily_limit \
--user <USER_ADDRESS> \
--limit 50000000Returns the current daily spending limit for the calling user, or None if no limit is set.
get_daily_limit(env: Env, user: Address) -> Option<i128>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<i128> — current daily limit in stroops, or None if unset.
Storage read: DataKey::DailyLimit(user) in temporary storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_daily_limit \
--user <USER_ADDRESS>Returns the amount spent today by the calling user via pay_per_use().
get_daily_spent(env: Env, user: Address) -> i128
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: i128 — amount spent today in stroops. Returns 0 if no spend is recorded.
Storage read: DataKey::DailySpent(user) in temporary storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_daily_spent \
--user <USER_ADDRESS>Extends the TTL of a user's subscription record in persistent storage.
extend_subscription_ttl(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to extend TTL for. |
Auth: None.
Storage written: Extends TTL of DataKey::Subscription(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- extend_subscription_ttl \
--user <USER_ADDRESS>Returns the trial end timestamp if the user is in a trial period.
get_trial_end(env: Env, user: Address) -> Option<u64>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<u64> — Unix timestamp when trial ends, or None if no trial or no subscription.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_trial_end \
--user <USER_ADDRESS>Sets the contract-wide grace period for charges. Only the contract admin can call this.
set_grace_period(env: Env, seconds: u64)
Parameters
| Name | Type | Description |
|---|---|---|
seconds |
u64 |
Number of seconds after the interval elapses during which charge() is still allowed. |
Auth: Admin only.
Storage written: DataKey::GracePeriod in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- set_grace_period \
--seconds 86400Adds a merchant to the whitelist. Only the contract admin can call this.
add_merchant(env: Env, merchant: Address)
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to whitelist. |
Auth: Admin only.
Storage written: DataKey::MerchantWhitelist(merchant) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- add_merchant \
--merchant <MERCHANT_ADDRESS>Removes a merchant from the whitelist. Only the contract admin can call this.
remove_merchant(env: Env, merchant: Address)
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to remove from the whitelist. |
Auth: Admin only.
Storage written: Removes DataKey::MerchantWhitelist(merchant) from persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- remove_merchant \
--merchant <MERCHANT_ADDRESS>Enables or disables the merchant whitelist. Only the contract admin can call this.
set_whitelist_enabled(env: Env, enabled: bool)
Parameters
| Name | Type | Description |
|---|---|---|
enabled |
bool |
True to enable the whitelist, false to disable. |
Auth: Admin only.
Storage written: DataKey::WhitelistEnabled in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- set_whitelist_enabled \
--enabled trueSets the protocol fee collection settings. Only the contract admin can call this.
set_fee(env: Env, collector: Address, bps: u32)
Parameters
| Name | Type | Description |
|---|---|---|
collector |
Address |
The address that will receive the protocol fees. |
bps |
u32 |
The fee amount in basis points (1 bps = 0.01%). |
Auth: Admin only.
Storage written: DataKey::FeeCollector and DataKey::FeeBps in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- set_fee \
--collector <COLLECTOR_ADDRESS> \
--bps 100Returns per-day revenue for the given merchant for the last days days, oldest to newest.
get_merchant_revenue_history(env: Env, merchant: Address, days: u32) -> Vec<i128>
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to query. |
days |
u32 |
The number of days of history to retrieve. |
Auth: None.
Returns: Vec<i128> — Daily revenue in stroops, ordered oldest to newest.
Storage read: DataKey::MerchantRevenueDay(merchant, day) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_merchant_revenue_history \
--merchant <MERCHANT_ADDRESS> \
--days 7Returns the referrer address recorded for a subscriber.
get_referrer(env: Env, user: Address) -> Option<Address>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<Address> — None if no referrer was recorded.
Storage read: DataKey::Referral(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_referrer \
--user <USER_ADDRESS>Upgrades contract storage to the latest schema version. Safe to call multiple times.
migrate(env: Env)
Auth: None (admin restriction can be added in future versions).
Storage written: DataKey::SchemaVersion in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- migrateReturns the current storage schema version.
get_schema_version(env: Env) -> u32
Auth: None.
Returns: u32 — defaults to 1 before the first migrate() call.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_schema_versionAttaches a short label string (e.g. plan name) to the caller's subscription.
set_metadata(env: Env, user: Address, label: String)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
label |
String |
Short display label (e.g. "pro", "basic"). |
Auth: user.require_auth().
Storage written: DataKey::SubscriptionMeta(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- set_metadata \
--user <USER_ADDRESS> \
--label proReturns the metadata label for a subscriber.
get_metadata(env: Env, user: Address) -> Option<String>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<String> — None if no label has been set.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_metadata \
--user <USER_ADDRESS>Returns the last (up to 12) charge timestamps for a subscriber, ordered oldest → newest.
get_charge_history(env: Env, user: Address) -> Vec<u64>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Vec<u64> — UNIX timestamps of successful charge() calls. Empty if no charges have occurred.
Storage read: DataKey::ChargeHistory(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_charge_history \
--user <USER_ADDRESS>All amounts are in stroops — the smallest unit of a Stellar token.
| Amount | Stroops |
|---|---|
| 1 XLM | 10,000,000 |
| 0.5 XLM | 5,000,000 |
| 0.0000001 XLM | 1 |
All intervals are in seconds.
| Interval | Seconds |
|---|---|
| 1 day | 86,400 |
| 1 week | 604,800 |
| 30 days | 2,592,000 |
All events can be indexed by listening to the Stellar RPC event stream for the FlowPay contract ID.
| Event name | Topic | Data |
|---|---|---|
subscribed |
("subscribed", user_address) |
(merchant, amount, interval) |
charged |
("charged", user_address) |
(merchant, amount, timestamp) |
pay_per_use |
("pay_per_use", user_address) |
(merchant, amount) |
cancelled |
("cancelled", user_address) |
() |
paused |
("paused", user_address) |
() |
resumed |
("resumed", user_address) |
() |
referred |
("referred", user_address) |
referrer_address |