Set it once. Pay forever. Decentralized subscriptions on Stellar.
SoroSubs is the first permissionless, on-chain recurring payment protocol built on Stellar/Soroban. Businesses, creators, and DAOs register subscription plans on-chain. Subscribers connect their Freighter wallet, approve a one-time USDC allowance, and the protocol autonomously charges each billing cycle — no credit cards, no bank approvals, no intermediaries.
The global subscription economy is worth over $1.5 trillion — and nearly all of it flows through Visa, Mastercard, and Stripe, which extract 2–4% per transaction. For a SaaS business processing $1M/year in subscriptions, that's $20,000–$40,000 in fees paid to intermediaries who add no product value. For creators in emerging markets, these payment rails are simply unavailable.
Every Netflix charge. Every Spotify payment. Every SaaS subscription renewal. It all flows through a small oligopoly of payment networks built in the 1970s.
Stellar was purpose-built for programmable money transfers:
| Property | Stellar | Ethereum | Traditional Rails |
|---|---|---|---|
| Finality | ~5 seconds | ~12 minutes | 1–3 business days |
| Transaction fee | <$0.001 | $1–$50+ | 2–4% |
| USDC support | Native (Circle-issued) | ERC-20 | USD only |
| Throughput | 1,000+ TPS | ~15 TPS | N/A |
| Access | Permissionless | Permissionless | KYC required |
Stellar's near-zero fees and 5-second finality make it the ideal settlement network for recurring micro-payments. A $9.99/month subscription processed on Stellar costs less than $0.001 in fees — vs. $0.40 with Stripe.
Until now, Stellar had no native protocol for programmable recurring payments. This gap meant:
- Builders had to create ad-hoc off-chain billing logic for every project
- No composable standard for subscriptions meant fragmented, non-interoperable implementations
- Stellar Anchors and payment apps couldn't offer subscription features to users
SoroSubs fills this gap by building directly on Soroban's token primitive: the SEP-41 approve + transfer_from mechanic. Subscribers grant a one-time USDC allowance; from that point forward, the protocol can pull payments without requiring a new signature each cycle.
SoroSubs directly enables:
- Subscription SaaS — Teams building on Stellar can offer recurring billing natively on-chain
- On-chain memberships — DAOs and communities can gate content behind verifiable, on-chain subscriptions
- Streaming payroll — Employers can create recurring salary disbursements using the same primitives
- DAO treasury automation — DAOs can automate recurring vendor payments and grants on-chain
- Stellar Anchors — Anchors (e.g., MoneyGram On-Ramp) can integrate subscription billing for fiat→USDC on-ramps
- Payment apps — LOBSTR, StellarX, and other wallets can surface subscription management natively
┌─────────────────────────────────────────────────────────┐
│ SoroSubs Protocol │
│ │
│ Merchant registers Plan (price + period) │
│ │ │
│ ▼ │
│ Subscriber approves USDC allowance to contract │
│ │ │
│ ▼ │
│ Subscriber calls subscribe() → first charge fires │
│ │ │
│ ▼ │
│ Keeper/bot calls charge() each cycle → transfer_from │
│ │ │
│ ▼ │
│ USDC flows: Subscriber → SoroSubs → Merchant │
│ └→ Treasury (fee) │
└─────────────────────────────────────────────────────────┘
approve + transfer_from as the primitive
Subscribers grant the SoroSubs contract a USDC allowance. The contract uses transfer_from to pull payments on schedule. Subscribers retain custody of their funds — the contract can only pull up to the approved amount.
Permissionless charge() function
Anyone can trigger a billing cycle once it's due. This enables keeper bots (cron services, keeper networks) to process charges at scale without centralized infrastructure. The charge_batch() function processes up to thousands of subscriptions per transaction.
Protocol fee in basis points
A configurable protocol fee (default: 50 bps = 0.5%) is deducted from each charge and forwarded to the treasury. This is transparent and auditable on-chain.
Non-custodial
SoroSubs never holds subscriber funds. The token flow is atomic: subscriber → contract → merchant + treasury within a single transaction.
sorosubs/
├── contracts/
│ └── sorosubs/
│ ├── Cargo.toml # Soroban contract manifest
│ └── src/
│ ├── lib.rs # Core contract logic
│ ├── types.rs # Data types, errors, events
│ └── test.rs # Full test suite (22 tests)
│
├── frontend/ # Next.js 14 app
│ ├── src/
│ │ ├── app/
│ │ │ ├── layout.tsx # Root layout
│ │ │ ├── page.tsx # Landing page
│ │ │ ├── dashboard/ # Merchant dashboard
│ │ │ ├── subscribe/ # Browse & subscribe to plans
│ │ │ └── manage/ # Manage active subscriptions
│ │ ├── components/
│ │ │ ├── Navbar.tsx
│ │ │ ├── PlanCard.tsx
│ │ │ └── ui.tsx # Shared UI primitives
│ │ ├── hooks/
│ │ │ └── useFreighter.ts # Freighter wallet hook
│ │ └── lib/
│ │ ├── constants.ts # Network config, formatters
│ │ └── sorosubs.ts # Contract interaction layer
│ └── package.json
│
├── sdk/ # sorosubs-sdk npm package
│ └── src/
│ ├── client.ts # SoroSubsClient class
│ ├── types.ts # TypeScript types
│ └── index.ts # Public API barrel
│
├── scripts/
│ └── deploy.js # Deployment scripts
│
└── package.json # Workspace root
| Function | Description |
|---|---|
initialize(admin, usdc_token, treasury, fee_bps) |
Deploy and configure the protocol (once) |
set_fee(fee_bps) |
Update the protocol fee (admin only) |
set_treasury(treasury) |
Update the fee treasury address (admin only) |
| Function | Description |
|---|---|
register_plan(merchant, name, price, period) |
Create a new subscription plan |
deactivate_plan(merchant, plan_id) |
Stop new subscriptions (existing unaffected) |
update_plan(merchant, plan_id, price, period) |
Change price/period (next cycle) |
| Function | Description |
|---|---|
subscribe(subscriber, merchant, plan_id) |
Subscribe + charge first cycle |
cancel(subscriber, merchant, plan_id) |
Cancel subscription (no refund) |
| Function | Description |
|---|---|
charge(subscriber, merchant, plan_id) |
Charge one subscription when due |
charge_batch(charges: Vec<(subscriber, merchant, plan_id)>) |
Batch charge, skips errors |
| Function | Description |
|---|---|
get_plan(merchant, plan_id) |
Fetch plan details |
get_subscription(subscriber, merchant, plan_id) |
Fetch subscription record |
stats() |
Global: total_plans, total_subs, fee_bps |
is_due(subscriber, merchant, plan_id) |
Check if billing is due |
| Code | Name | Description |
|---|---|---|
| 1 | Unauthorized |
Caller is not authorized |
| 2 | PlanNotFound |
Plan does not exist |
| 3 | PlanInactive |
Plan not accepting subscribers |
| 4 | SubscriptionNotFound |
No subscription found |
| 5 | SubscriptionInactive |
Subscription already cancelled |
| 6 | BillingNotDue |
Too early to charge |
| 7 | InsufficientAllowance |
USDC allowance too low |
| 8 | InsufficientBalance |
Subscriber USDC balance too low |
| 9 | InvalidPrice |
Price must be > 0 |
| 10 | InvalidPeriod |
Period must be ≥ 60s |
| 11 | InvalidName |
Name must be 1–64 chars |
| 12 | InvalidFeeBps |
Fee cannot exceed 10% |
| 13 | AlreadyInitialized |
Contract already deployed |
| 14 | AlreadySubscribed |
Already has active subscription |
# Rust + Soroban target
rustup target add wasm32-unknown-unknown
# Stellar CLI
cargo install --locked stellar-cli
# Node.js 18+
node --version# Run tests
cd contracts/sorosubs
cargo test --features testutils
# Build WASM
cargo build --target wasm32-unknown-unknown --release
# Or from repo root
npm run build:contract
npm run test:contract# Create a deployer identity
stellar keys generate deployer --network testnet
stellar keys fund deployer --network testnet
# Deploy contract
stellar contract deploy \
--wasm contracts/sorosubs/target/wasm32-unknown-unknown/release/sorosubs.wasm \
--source deployer \
--network testnet
# Initialize (replace with actual addresses)
stellar contract invoke \
--id $CONTRACT_ID \
--source deployer \
--network testnet \
-- initialize \
--admin $DEPLOYER_ADDRESS \
--usdc_token $USDC_CONTRACT_ID \
--treasury $TREASURY_ADDRESS \
--fee_bps 50# Copy env template
cp frontend/.env.example frontend/.env.local
# Edit .env.local with your contract ID
NEXT_PUBLIC_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
NEXT_PUBLIC_NETWORK=TESTNET
# Install and run
npm install
npm run dev:frontendnpm install sorosubs-sdk @stellar/stellar-sdkimport { SoroSubsClient, TESTNET_CONFIG, usdcToStroops } from "sorosubs-sdk";
const client = new SoroSubsClient({
...TESTNET_CONFIG,
contractId: "C...", // your deployed contract
});
// Merchant: register a plan ($9.99/month)
const planId = await client.registerPlan(
merchantSigner,
"Pro Monthly",
usdcToStroops(9.99),
30 * 24 * 60 * 60 // 30 days
);
// Subscriber: approve 12 months of USDC and subscribe
await client.approveUsdc(
subscriberSigner,
usdcToStroops(9.99) * 12n,
6_312_000 // expiry ledger ~1 year
);
await client.subscribe(subscriberSigner, merchantAddress, planId);
// Keeper: batch charge all due subscriptions
await client.chargeBatch(keeperSigner, [
{ subscriber: "G...", merchant: "G...", planId: 0 },
{ subscriber: "G...", merchant: "G...", planId: 0 },
]);
// Check stats
const stats = await client.getStats();
console.log(`${stats.total_subs} active subscriptions`);Create frontend/.env.local:
NEXT_PUBLIC_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
NEXT_PUBLIC_USDC_CONTRACT_ID=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
NEXT_PUBLIC_NETWORK=TESTNET
NEXT_PUBLIC_RPC_URL=https://soroban-testnet.stellar.orgSoroSubs uses USDC with 7 decimal places (Stellar standard):
| Human amount | Stroops |
|---|---|
| $1.00 USDC | 10_000_000 |
| $9.99 USDC | 99_900_000 |
| $100.00 USDC | 1_000_000_000 |
Use the SDK helpers:
import { usdcToStroops, stroopsToUsdc } from "sorosubs-sdk";
usdcToStroops(9.99) // → 99_900_000n
stroopsToUsdc(99_900_000n) // → 9.99- Allowance exposure: Subscribers should approve only the amount they expect to pay. Over-approving (e.g.,
i128::MAX) is inadvisable. The protocol reads the allowance before each charge. - Inactive subscriptions: Cancelled subscriptions are permanently inactive. A new subscription must be created to resume billing.
- Keeper trust:
charge()is permissionless, but it can only execute whennow >= next_bill_at. Early charges are rejected withBillingNotDue. - Plan changes: Merchants can update price/period, but changes only apply to the next billing cycle. Subscribers are never silently charged a different amount mid-cycle.
- Admin key: The admin address can only update fee settings and treasury. It cannot touch subscriber funds or cancel plans.
- Auditing: All billing events are emitted on-chain (
billed,subscribed,cancelled) and can be indexed by any observer.
- Indexer: Graph-protocol-style indexer for querying plans and subscriptions by address
- Keeper Network: Decentralized keeper bot with on-chain reward for triggering
charge() - Free trial support:
subscribe_with_trial(trial_period)— first charge deferred - Metered billing:
charge_metered(amount)— variable-amount billing based on usage - Multi-token support: Accept any Stellar asset, not just USDC
- Webhooks: Off-chain notification service for billing events
- Discount codes: On-chain promo code support for reduced pricing
Pull requests welcome. For major changes, please open an issue first to discuss what you'd like to change.
- Fork the repo
- Create a feature branch (
git checkout -b feat/my-feature) - Run the test suite (
cargo test --features testutils) - Submit a PR
MIT © SoroSubs Contributors