Soroban smart contracts for PayStream — decentralized payroll and salary streaming on the Stellar blockchain.
PayStream lets employers stream salaries to employees in real-time, per-second. Instead of waiting for a monthly paycheck, employees earn and can withdraw their salary continuously as they work — fully on-chain, trustless, and transparent.
🎬 Watch the demo — see the full
create_stream → withdrawflow in action.
- Real-time pay — employees access earned wages any time, not just payday
- Trustless escrow — funds locked on-chain; employer cannot claw back earned salary
- Transparent — every stream, withdrawal, and cancellation is an immutable on-chain event
- Stellar-native — built on Stellar's fast, low-fee infrastructure with Soroban smart contracts
- Flexible — pause, resume, top-up, or cancel streams; optional hard stop time
- Multi-token — each stream can use any SEP-41 compliant token; employer and employee can run concurrent streams in different assets
.
├── contracts
│ ├── stream # Core salary streaming and escrow contract
│ │ ├── src
│ │ │ ├── lib.rs # Stream entrypoints
│ │ │ ├── storage.rs # Persistence and claimable calculation
│ │ │ ├── events.rs # On-chain event publishing
│ │ │ ├── types.rs # Domain models and storage keys
│ │ │ └── test.rs # Contract tests
│ │ └── Cargo.toml
│ └── token # Fungible payment token contract
│ ├── src
│ │ ├── lib.rs
│ │ ├── storage.rs
│ │ ├── types.rs
│ │ └── test.rs
│ └── Cargo.toml
├── scripts
│ ├── build.sh
│ ├── deploy-local.sh
│ ├── deploy-testnet.sh
│ └── init-testnet.sh
├── Cargo.toml
├── Makefile
├── CONTRIBUTING.md
├── SECURITY.md
└── README.md
- Rust (latest stable)
- Stellar CLI
git clone https://github.com/Vera3289/paystream-contracts.git
cd paystream-contracts
rustup target add wasm32-unknown-unknownmake build
# or: stellar contract buildmake test
# or: cargo testmake fmt-check
make lintBuild and test entirely inside Docker — no local Rust or Stellar CLI installation needed.
Run tests:
docker compose run --rm testBuild contracts only:
docker compose run --rm build stellar contract buildThe cargo-cache volume persists the Cargo registry between runs so subsequent builds are fast.
Full parameter, return value, error, and example documentation: docs/api-reference.md
Developer quickstart tutorial (zero to running stream in 30 min): docs/quickstart.md
SDK examples (JavaScript, Python, Rust): examples/
Frontend integration guide (TypeScript): docs/integration/frontend.md
| Function | Caller | Description |
|---|---|---|
initialize(admin) |
Admin | Set contract admin |
create_stream(employer, employee, token, deposit, rate_per_second, stop_time) |
Employer | Create stream, lock deposit |
create_streams_batch(employer, params) |
Employer | Create multiple streams atomically; all succeed or all revert |
withdraw(employee, stream_id) |
Employee | Withdraw all claimable earnings |
top_up(employer, stream_id, amount) |
Employer | Add more funds to active stream |
pause_stream(employer, stream_id) |
Employer | Pause accrual |
resume_stream(employer, stream_id) |
Employer | Resume accrual |
cancel_stream(employer, stream_id) |
Employer | Pay employee earned share, refund remainder |
get_stream(stream_id) |
Anyone | Read stream state |
claimable(stream_id) |
Anyone | Query withdrawable amount right now |
stream_count() |
Anyone | Total streams created |
| Approach | Transactions | Approx. fee |
|---|---|---|
N individual create_stream calls |
N | N × base fee |
One create_streams_batch call |
1 | 1 × base fee + per-stream resource overhead |
create_streams_batch is cheaper for N ≥ 2 because Stellar charges one base fee per transaction. Per-stream resource overhead grows linearly but is far smaller than the per-transaction base fee saved.
Active → Paused → Active
Active → Cancelled
Active → Exhausted (deposit fully streamed)
claimable = min(
(now - last_withdraw_time) * rate_per_second,
deposit - withdrawn
)
Time is capped at stop_time if set. Paused time is excluded.
./scripts/build.sh
./scripts/deploy-testnet.sh
export STELLAR_ADMIN_ADDRESS=<YOUR_PUBLIC_KEY>
export TOKEN_CONTRACT_ID=<FROM_DEPLOY>
export STREAM_CONTRACT_ID=<FROM_DEPLOY>
./scripts/init-testnet.shmake deploy-local| Layer | Technology |
|---|---|
| Blockchain | Stellar (Soroban) |
| Language | Rust |
| SDK | Soroban SDK v22.0.0 |
| CI/CD | GitHub Actions |
PayStream is token-agnostic — any SEP-41 compliant token works. The recommended default is Stellar USDC issued by Circle.
| Network | Contract Address |
|---|---|
| Testnet | GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 |
| Mainnet | GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN |
Source: Circle — Stellar USDC
import { CONFIG, USDC } from "./config";
// 1 USDC = 10_000_000 stroops (7 decimal places on Stellar)
const ONE_USDC = 10_000_000n;
await contract.create_stream({
employer: myPublicKey,
employee: employeePublicKey,
token_address: USDC.testnet, // swap for USDC.mainnet in prod
deposit: ONE_USDC * 3600n, // 3600 USDC
rate_per_second: ONE_USDC, // 1 USDC / second
stop_time: 0n, // no hard stop
cooldown_period: 0n,
cliff_time: 0n,
});The demo UI pre-fills the token field with the testnet USDC address automatically via CONFIG.defaultToken.
See CONTRIBUTING.md.
See SECURITY.md. Report vulnerabilities to [email protected] — not via public issues.
Built with ❤️ on Stellar