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.
New here? Create your first testnet stream in 5 minutes with the Quickstart.
- 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
- Native XLM — stream Stellar's native asset via its Stellar Asset Contract (SAC) address; no custom token deployment needed (see docs/testnet.md)
Solid arrows are contract calls made by each actor; dashed arrows are token transfers.
flowchart LR
Admin([Admin])
Employer([Employer])
Employee([Employee])
SC[StreamContract]
TC[TokenContract<br/>SEP-41 / SAC]
Admin -->|"initialize, propose_admin / accept_admin,<br/>pause_contract / unpause_contract,<br/>set_min_deposit, upgrade, migrate"| SC
Employer -->|"create_stream, create_streams_batch, top_up,<br/>pause_stream, pause_stream_batch, resume_stream, resume_stream_batch, update_rate,<br/>cancel_stream, cancel_streams_batch"| SC
Employee -->|"withdraw, withdraw_all"| SC
SC -->|"transfer"| TC
Employer -.->|"deposit / top-up"| SC
SC -.->|"earned salary<br/>(withdraw, cancel)"| Employee
SC -.->|"unearned refund (cancel)"| Employer
Anyone can call the read-only functions (get_stream, claimable, stream_status, …) and settle_stream.
stateDiagram-v2
[*] --> Active: create_stream
Active --> Paused: pause_stream / pause_stream_batch
Paused --> Active: resume_stream / resume_stream_batch
Active --> Cancelled: cancel_stream
Paused --> Cancelled: cancel_stream
Active --> Exhausted: withdraw / settle_stream (deposit fully paid or stop_time passed)
Cancelled --> [*]
Exhausted --> [*]
See the glossary for terms and the client guide for JavaScript integration examples.
.
├── 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
│ ├── export-streams.sh # CSV/JSON export of an employer's streams
│ ├── init-testnet.sh
│ └── sync-labels.sh # Apply .github/labels.yml to the repository
├── Cargo.toml
├── Makefile
├── CONTRIBUTING.md
├── SECURITY.md
└── README.md
- Rust (latest stable)
- Stellar CLI
git clone https://github.com/veracindarella/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
| Function | Caller | Description |
|---|---|---|
initialize(admin) |
Admin | Set contract admin |
create_stream(employer, employee, token, deposit, rate_per_second, stop_time, low_balance_threshold) |
Employer | Create stream, lock deposit; optional low_bal event threshold |
create_streams_batch(employer, params) |
Employer | Create multiple streams atomically; all succeed or all revert |
withdraw(employee, stream_id) |
Employee | Withdraw all claimable earnings |
withdraw_all(employee) |
Employee | Withdraw from all streams in one transaction |
top_up(employer, stream_id, amount, low_balance_threshold) |
Employer | Add more funds to active stream; optionally update low_bal threshold |
set_low_balance_threshold(employer, stream_id, threshold) |
Employer | Adjust or disable (0) alert threshold without topping up |
pause_stream(employer, stream_id) |
Employer | Pause accrual |
pause_stream_batch(employer, stream_ids) |
Employer | Pause multiple streams atomically; all succeed or all revert (max 50) |
resume_stream(employer, stream_id) |
Employer | Resume accrual |
resume_stream_batch(employer, stream_ids) |
Employer | Resume multiple paused streams atomically; all succeed or all revert (max 50) |
cancel_stream(employer, stream_id) |
Employer | Pay employee earned share, refund remainder |
cancel_streams_batch(employer, stream_ids) |
Employer | Cancel multiple streams atomically; all succeed or all revert |
get_stream(stream_id) |
Anyone | Read stream state |
get_streams_batch(stream_ids) |
Anyone | Read up to 50 streams in one call, in input order |
get_auto_topup_config(stream_id) |
Anyone | Query recurring auto top-up config for a stream |
stream_status(stream_id) |
Anyone | Query stream status only (lightweight) |
claimable(stream_id) |
Anyone | Query withdrawable amount right now |
stream_count() |
Anyone | Total streams created |
stream_count_by_employer(employer) |
Anyone | Number of streams owned by an employer |
stream_count_by_employee(employee) |
Anyone | Number of streams paying an employee |
| 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
To report a vulnerability, please do not open a public issue. Instead, email the security team at security@paystream.dev. Include a description of the issue, reproduction steps, and any relevant logs. We aim to acknowledge reports within 48 hours.
For encrypted reporting, use our PGP key (fingerprint published in SECURITY.md) or contact us via Signal at the number listed there.
See SECURITY.md for the full disclosure policy and supported versions.