Automated recurring subscription protocol for Stellar/Soroban.
On Stellar, every payment requires the sender to sign a transaction. This makes recurring billing (SaaS subscriptions, streaming salaries, payroll) awkward — the payer must be online and re-sign each cycle.
HorizonFlow lets a payer pre-authorise a payee to pull a fixed token amount at a set interval, up to an optional cap, without any further interaction. The logic lives in a Soroban smart contract; no trusted third-party needed.
- Pull-payment model: payee initiates each transfer, payer is passive after setup
- SEP-41 / SAC compatible (works with USDC, XLM, any Stellar asset)
- Fixed interval enforcement using ledger timestamps only
- Optional max-periods cap; subscription auto-completes when reached
- Payer can cancel at any time; no further pulls succeed
- Typed errors — no panics for expected failure cases
- Indexed views by payer and payee address
- Lean single-crate contract; only
soroban-sdkdependency
# Rust stable toolchain
rustup target add wasm32-unknown-unknown
# Soroban CLI (replace X.Y.Z with latest)
cargo install --locked soroban-cli --version X.Y.Z
cargo build --target wasm32-unknown-unknown --release \
--manifest-path contracts/horizonflow/Cargo.toml
cargo test --manifest-path contracts/horizonflow/Cargo.toml
# 1. Configure testnet network
soroban network add testnet \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015"
# 2. Create / fund a deployer identity
soroban keys generate deployer --network testnet --fund
# 3. Deploy
soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/horizonflow.wasm \
--network testnet \
--source deployer
# The command prints a CONTRACT_ID — save it.
All functions live on the single HorizonFlowContract.
create_subscription(
payer: Address,
payee: Address,
token: Address,
amount_per_period: i128,
interval_seconds: u64,
max_periods: Option<u32>, // None = unlimited
start_time: u64, // ledger timestamp gate
) -> Result<u64> // returns subscription ID
Requires payer auth. Emits SubscriptionCreated.
pull_payment(id: u64) -> Result<()>
Requires payee auth. Validates interval, transfers token, emits PaymentPulled.
cancel_subscription(id: u64) -> Result<()>
Requires payer auth. Sets status to Cancelled, emits SubscriptionCancelled.
| Function | Returns |
|---|---|
get_subscription(id) |
Result<Subscription> |
list_subscriptions_by_payer(addr) |
Vec<u64> |
list_subscriptions_by_payee(addr) |
Vec<u64> |
is_due(id) |
bool |
# Create subscription (30 USDC / month, max 12 months)
soroban contract invoke \
--id $CONTRACT_ID --network testnet --source payer \
-- create_subscription \
--payer $PAYER_ADDR --payee $PAYEE_ADDR --token $USDC_ADDR \
--amount_per_period 30000000 --interval_seconds 2592000 \
--max_periods 12 --start_time $(date +%s)
# Pull a payment (run as payee)
soroban contract invoke \
--id $CONTRACT_ID --network testnet --source payee \
-- pull_payment --id 1
# Cancel (run as payer)
soroban contract invoke \
--id $CONTRACT_ID --network testnet --source payer \
-- cancel_subscription --id 1
# Check if due
soroban contract invoke \
--id $CONTRACT_ID --network testnet --source anyone \
-- is_due --id 1
| Code | Name | Meaning |
|---|---|---|
| 1 | NotAuthorized |
Caller lacks required signature |
| 2 | SubscriptionInactive |
Subscription cancelled or completed |
| 3 | TooEarlyToPull |
Interval not elapsed / before start_time |
| 4 | MaxPeriodsReached |
All authorised periods paid |
| 5 | InsufficientBalance |
Payer token balance/allowance too low |
| 6 | InvalidParams |
Bad create parameters |
- ARCHITECTURE.md — design decisions, state model, security
- CONTRIBUTING.md — dev setup, good first issues
- SECURITY.md — vulnerability reporting
- CHANGELOG.md — version history
- examples/client-usage-example.md — end-to-end code snippets
MIT — see LICENSE.