Common questions from integrators, developers, and employers using PayStream.
PayStream is a set of Soroban smart contracts on the Stellar blockchain that let employers stream salary to employees in real time, per second. Instead of a monthly paycheck, employees earn and can withdraw continuously as they work.
Any SEP-41 compliant token contract. This includes Stellar Asset Contracts (SACs) wrapping native XLM or any Stellar classic asset, as well as custom token contracts that implement the SEP-41 interface (balance, transfer, approve, etc.).
The token address is validated at stream creation time via a try_balance probe. Passing an address that does not implement SEP-41 will be rejected with E012.
Yes. Each create_stream call creates an independent stream with its own deposit, rate, and token. An employer can have any number of active streams to different employees, in different tokens.
Yes. Employees can receive any number of concurrent streams. Use streams_by_employee(employee) to list all stream IDs paying a given address.
The admin sets a global minimum deposit via set_min_deposit. The default is 10 000 stroops (0.001 XLM equivalent). Attempting to create a stream with a deposit below this value will be rejected with E007.
1 000 000 000 (one billion) tokens per second. Rates above this are rejected with E008 to prevent arithmetic overflow in the claimable calculation.
Yes. Pass a Unix timestamp (seconds) as stop_time. The stream will stop accruing at that time even if the deposit is not fully exhausted. Pass 0 for an indefinite stream.
An optional minimum number of seconds the employee must wait between withdrawals. Set to 0 to allow withdrawals at any time. Withdrawing before the cooldown expires is rejected with E010.
claimable = min(
(now - last_withdraw_time) * rate_per_second,
deposit - withdrawn
)
Time is capped at stop_time if set. Paused intervals are excluded because last_withdraw_time is reset to the current timestamp on resume_stream.
The stream transitions to Exhausted status. The employee can still call withdraw on an Exhausted stream — it returns 0 without reverting. No further accrual occurs.
Optionally. The admin can configure a fee in basis points (1 bps = 0.01%) up to a maximum of 100 bps (1%) via set_protocol_fee. The fee is deducted from the withdrawal amount and sent to the configured fee_recipient. The default fee is 0 (disabled).
| Code | Constant | Meaning |
|---|---|---|
| E001 | ERR_ZERO_RATE |
rate_per_second must be > 0 |
| E002 | ERR_ZERO_DEPOSIT |
deposit must be > 0 |
| E003 | ERR_REENTRANT |
Reentrant withdraw detected (defence-in-depth guard) |
| E004 | ERR_OVERFLOW |
Arithmetic overflow in claimable calculation |
| E005 | ERR_STREAM_CANCELLED |
Cannot top up a cancelled stream |
| E006 | ERR_STREAM_EXHAUSTED |
Cannot top up an exhausted stream |
| E007 | ERR_BELOW_MIN_DEPOSIT |
Deposit is below the configured minimum |
| E008 | ERR_INVALID_RATE |
rate_per_second exceeds the maximum of 1 000 000 000 |
| E009 | ERR_BAD_NONCE |
Admin nonce mismatch — replay protection triggered |
| E010 | ERR_WITHDRAW_COOLDOWN |
Withdrawal attempted before cooldown period expired |
| E011 | ERR_FEE_TOO_HIGH |
fee_bps exceeds the maximum of 100 |
| E012 | ERR_INVALID_TOKEN |
Token address is not a valid SEP-41 contract |
| E013 | ERR_UNAUTHORIZED_TRANSFER |
Caller is not the pending employer for this stream |
Yes, via a two-step process to prevent accidental transfers:
- Current employer calls
propose_employer_transfer(stream_id, new_employer). - New employer calls
accept_employer_transfer(new_employer, stream_id).
Until the new employer accepts, the current employer retains full control. After acceptance, the old employer loses all control and the new employer gains it.
| Aspect | Testnet | Mainnet |
|---|---|---|
| Network passphrase | Test SDF Network ; September 2015 |
Public Global Stellar Network ; September 2015 |
| Friendbot funding | Available at https://friendbot.stellar.org |
Not available — fund from exchange |
| Token contracts | Use testnet SACs or deploy your own test token | Use real asset SACs |
| Ledger close time | ~5 seconds | ~5 seconds |
| Data persistence | Testnet resets periodically | Permanent |
| Contract IDs | Different from mainnet | — |
Always test on testnet before deploying to mainnet. See docs/testnet.md for the full testnet deployment guide.
Stellar charges a base fee per transaction plus resource fees for CPU, memory, and storage. For a single create_stream call, expect approximately 100–500 stroops in total fees on mainnet under normal network load. Use stellar transaction simulate to get an exact fee estimate before submitting.
For batch creation (create_streams_batch), one base fee covers all streams in the batch, making it significantly cheaper than N individual calls for N ≥ 2.
Call admin_nonce() before any admin operation to get the current nonce, then pass it as the nonce argument. The nonce increments after each successful admin call. This prevents replay attacks where a signed admin transaction is submitted more than once.
const nonce = await contract.admin_nonce();
await contract.set_min_deposit({ admin, nonce, amount: 50_000n });Yes. All state changes emit on-chain events that can be indexed by Horizon or a custom indexer. Key event topics:
| Topic | Emitted by |
|---|---|
created |
create_stream, create_streams_batch |
withdraw |
withdraw |
status |
pause_stream, resume_stream, cancel_stream |
topup |
top_up |
paused |
pause_contract, unpause_contract |
emp_prop |
propose_employer_transfer |
emp_acc |
accept_employer_transfer |
See docs/events.md for the full event schema.