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
}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
}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)
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). |
Auth: user.require_auth() — the transaction must be signed by user.
Storage written: DataKey::Subscription(user) in persistent storage. last_charged is set to the current ledger timestamp.
Events emitted
topic: ("subscribed", user)
data: (merchant, amount, interval)
Errors
| Condition | Panic message |
|---|---|
amount <= 0 |
"amount must be positive" |
interval == 0 |
"interval must be positive" |
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 - Calls
transfer_from(contract, user, merchant, 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" |
| 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>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) |
() |