Project brief · Stellar · Soroban smart contract

Fund your dependencies on-chain.

Upstream routes a share of everything a project earns to the contributors and open-source libraries it is built on, and lets those libraries do the same for theirs.

Contract CCNQHI3PBXFDMGOM32WAY5UB6WLPU2X4RLK6XF7FRRFAZ3GCHRVY5POG
Deployment 14 SEP 2026
Network
Stellar testnet
Protocol 28
Toolchain
soroban-sdk
27.0.6
Weight scale
1,000,000 ppm
Recipients
up to 50 per split
Wasm size
13.7 KB of 128 KiB
Tests
15 passing
License Apache-2.0
1,000,000
ppm · weights sum exactly
50
recipients per split
0.0088
XLM · a steady-state deposit
< 1%
of the per-tx CPU limit
0
admin keys, fees, pause switches
01

The problem

Every app on Stellar runs on libraries someone else maintains: SDKs, wallet kits, oracles, indexers. The money the ecosystem earns rarely reaches those maintainers. Funding happens project by project, and the dependencies underneath stay invisible.

The SCF Public Goods Working Group wants to change that. Its plans include a Dependency-Matching Pool, retroactive public-goods funding, and a Dependency-Match Bonus that credits a project's top dependencies when it reaches mainnet. PG Atlas already maps the ecosystem's dependency graph and scores how critical each component is.

What's missing is the rail that moves the money. Stellar has no trustless, on-chain way to say "10% of everything this project receives goes to these dependencies, weighted like this," and have it happen without an administrator pressing a button. The one splitter SCF has funded is admin-triggered and has not been updated since October 2024. Drips, the best-known dependency-funding protocol, runs only on EVM chains; on Stellar it operates a bounty program, not its funding contracts.

What exists today
  • An admin-triggered splitter, last updated October 2024
  • Drips, EVM-only; a bounty program on Stellar
  • Dependency graphs, but no rail to pay them
02

What Upstream is

Upstream is a single Soroban contract that holds many splits. A split is a list of up to 50 weighted recipients, where weights are parts per million and must total exactly 1,000,000. A recipient is one of two things:

Recipient type 01

An account.

Any Stellar wallet (G…) or contract (C…), paid when it collects.

Recipient type 02

Another split.

Its share flows into that split, which divides it again. This is how a project funds a dependency list, and how each dependency funds its own.

Anyone can deposit into a split: a grant tranche, app revenue, a bounty pool, an agent payment. Upstream accepts any Stellar asset contract or SEP-41 token, and keeps separate books for each.

Fig. 01 · one Upstream contract holds every split's balance
idle
deposit collect pull collect collect
any SAC token
Funder
Split #1 · app
epoch 0
acc 0
Maintainer
900,000 ppm
claimable 0
Split #0 · deps
100,000 ppm
acc 0
lib-one
600,000 ppm
claimable 0
lib-two
400,000 ppm
claimable 0
A two-level dependency tree. Money enters once with deposit; accounts leave with collect; nested splits receive their share with pull, one hop at a time.
03

How money moves

Upstream never pays a whole recipient list in one transaction. Money moves in three independent calls, each small and bounded.

Call 01

Deposit

A funder sends tokens into a split. The contract measures how much it actually received and adds that to the split's running total for the current epoch. The cost is the same for a split with two recipients or fifty, which is what makes frequent small payments viable: a steady-state deposit on testnet costs 0.0088 XLM.

Signed by from
Call 02

Collect

Anyone can trigger an account recipient's payout, but the tokens only ever go to that recipient, so keepers, bots or friends can settle on someone's behalf without being able to redirect anything. A recipient that wants its funds elsewhere, such as an exchange deposit address with a memo ID, signs collect_to and picks the destination.

Signed by anyone
Call 03

Pull

Anyone can move a nested split's share into that split's own current epoch. Deep dependency trees resolve one hop per call, so no transaction ever has to walk an unbounded graph.

Signed by anyone
Push: one call pays everyone 1 transaction
Splitter
  • alicepending
  • bobpending
  • carol · no trustlinepending
One transaction pays alice, bob and carol. Run it.
Upstream: each recipient settles alone 0 of 3 transactions
Split
  • alice
  • bob
  • carol · no trustline
Three separate transactions. Settle each recipient yourself.
Fig. 02 · Why Upstream is pull-based. On Stellar, sending a token to an account without a trustline fails. A push payout lets one such recipient block everyone; Upstream isolates the failure to that recipient. This case is covered by a unit test.
04

Guarantees

Money already received cannot be redirected

Changing a split's recipients starts a new epoch. Everything credited under the old recipient list stays with those recipients permanently; only deposits made after the change follow the new list. A split owner cannot quietly reroute funds that already arrived, and old epochs stay claimable forever.

step 0 of 5
create_split(owner, [alice 1,000,000], false) → id 1, epoch 0
epoch 0current
alice · 1,000,000 ppm
acc0
claimable0
paid out0
epoch 1not created
bob · 1,000,000 ppm
acc
claimable
paid out
Epochs make updates safe and cheap. The update writes one new recipient list; it never touches balances already credited to epoch 0.

More properties

Permanence on demand.lock freezes a split's recipients forever, or a split can be created locked.
No trusted operator.No admin key, no fees, no pause switch, no upgrade path. Only a split's owner controls that split, and handing it over needs both the current and the new owner to sign.
Honest deposits.Credits equal the contract's measured balance change, never more than the amount requested, so fee-on-transfer tokens are accounted correctly.
Isolated tokens.Balances are tracked per split, per epoch, per token. A broken or hostile token contract can only affect its own books.
05

The accounting

For every split, epoch and token, Upstream stores acc: the total amount ever credited. For every recipient that has settled, it stores a checkpoint of acc at that moment. A recipient with weight w is owed:

owed = ⌊ w × acc / 1,000,000 ⌋ − ⌊ w × checkpoint / 1,000,000 ⌋
then  checkpoint ← acc

Because entitlement is computed from the lifetime total rather than deposit by deposit, rounding never compounds. Each recipient loses at most one base unit per epoch, and that dust stays in the contract.

Fig. 04 · 30 deposits of 10 units, recipients collect after every deposit
deposit 0 / 30
Recipient Weight (ppm) Per-deposit rounding Upstream
alice333,33300
bob333,33300
carol333,33400
Lost to rounding of 000
Arithmetic is checked everywhere, and a deposit is refused if it would push weight × acc past what an i128 can hold, so settlement can never overflow.
Fig. 05 · build a split
3 recipients
XLM = 10,000,000,000 stroops
total
dust retained: 0 stroops
1,000,000
Weights always resolve to exactly 1,000,000 ppm. Payouts are floored to the base unit, so whatever the weights, the contract keeps at most one stroop per recipient.
06

Built for Soroban

Each design choice answers a specific property of the Stellar network, verified against live mainnet settings.

Network property How Upstream handles it
Token transfers to an account without a trustline failPer-recipient pull settlement; a failed collect rolls back and its share stays claimable
Per-transaction limits: 400M CPU instructions, 200 ledger writesLargest call uses about 1.98M instructions and 4 writes, under 1% of either limit
Storage expires: persistent entries start with 120 days (2,073,600 ledgers)Touched entries extend toward 120 days, only when at least a day is due, and by at most 30 days per call
Expired entries archiveUncollected balances are never lost; the next transaction that touches them restores them automatically
Storage rent: 1,000 stroops per KB on mainnet todayAbout 0.17 XLM per KB per 120 days; a 50-recipient list is 6.4 KB
Muxed addresses cannot be stored as keysRecipients are stored as base addresses; collect_to accepts a muxed destination
RPC keeps events for about 7 daysEvery state change emits a typed event with split id and token as topics, ready for a long-lived indexer
Re-entrant calls are blocked by the hostState is settled before every outgoing transfer regardless
Found during development: the SDK's local fee estimator prices storage rent at 12,000 stroops per KB, a deliberate overestimate. Live mainnet charges the 1,000 stroop floor, so local estimates overstate rent twelvefold.
07

Contract interface

Function Signed by What it does
create_split(owner, recipients, locked)ownerCreates a split and returns its id
update_recipients(split_id, recipients)ownerReplaces recipients and starts a new epoch; fails if locked
lock(split_id)ownerFreezes recipients permanently
transfer_ownership(split_id, new_owner)both ownersHands the split to a new owner
deposit(from, split_id, token, amount)fromFunds a split; returns the amount credited
pull(parent_id, parent_epoch, child_id, token)anyoneMoves a nested split's share into it
collect(split_id, epoch, account, token)anyonePays an account its share, to itself
collect_to(split_id, epoch, account, token, to)accountPays the share to any address, including muxed
claimable(split_id, epoch, payee, token)read onlyAmount a payee could settle now
get_split · get_recipients · get_received · next_split_idread onlyViews
Events
  • split_created
  • recipients_updated
  • split_locked
  • ownership_transferred
  • deposited
  • pulled
  • collected
08

Verification

Unit tests

Tests run against the Soroban host with mainnet resource limits and mainnet storage settings enforced.

  1. 01Each recipient is paid exactly its weight
  2. 02Rounding never overpays and never compounds
  3. 03Collect needs no signature but pays only the recipient
  4. 04Collect-to requires the recipient's signature
  5. 05A recipient without a trustline only blocks itself
  6. 06Updates start an epoch and protect received funds
  7. 07Locked splits cannot be updated
  8. 08Owner actions require the owner's signature
  9. 09Ownership transfer moves control
  10. 10Nested splits route shares to dependencies
  11. 11Pull rejects a split that is not a recipient
  12. 12Recipient lists are validated
  13. 13Deposits are validated
  14. 14Tokens are accounted separately
  15. 15A 50-recipient split stays within network limits

Live on testnet

The contract is deployed on Stellar testnet. A split paying alice 700,000 ppm and bob 300,000 ppm received deposits of 10 XLM and 1 XLM. Alice collected exactly 7 XLM and then 0.7 XLM; bob collected exactly 3 XLM. Every transfer and contract event appeared on-chain as expected.

Call on testnetFee charged
deposit, steady state0.0088 XLM
collect, steady state0.0242 XLM
First calls on a fresh deployment3.1–4.0 XLM

The first-call fees are almost entirely one-time storage rent: the first deposit paid 40.29M stroops of rent against 16.7K stroops of compute. Testnet's minimum storage lifetime is 7 days, so early calls topped the contract, its code and new entries up toward the 120-day target. Mainnet entries start at 120 days, so these become small daily extensions.

Estimated mainnet costs

Call CPU Writes Est. fee
create_split, 50 recipients1.96M41.06 XLM
deposit, first for a token0.38M40.075 XLM
deposit, subsequent0.41M40.011 XLM
collect, first time0.50M30.078 XLM
pull0.31M20.062 XLM
update_recipients, 50 recipients1.98M31.03 XLM

Estimated with mainnet storage settings and the live rent rate. They exclude Wasm execution overhead, so treat them as lower bounds until confirmed on mainnet.

09

Landscape

Others have built payment splitting on Stellar. Upstream's focus is the guarantees dependency funding needs.

Property Upstream Tributary SoroSplits Drips
StatusTestnet, unauditedTestnet, unaudited, activeSCF #19 and #23 ($153.7K); last update Oct 2024Production on EVM chains; on Stellar, bounty program only
PayoutsPer-recipient pull, anyone triggersPush pay(), or deposit then distribute()Admin triggers, recipients withdrawPull-based
Changing recipients after funds arriveNew epoch; received funds protectedController updates; no epochs documentedEditable until lockedOwner updates
Nested splitsYes, one hop per pullYesManualYes, Drip Lists
RoundingAt most 1 unit per recipient per epochDust to the last recipient each payoutBasis pointsDust stays with the splitter
SDK and dashboardNot yetYesYesYes

Tributary and Drips details come from their public documentation as of September 2026.

10

Limitations

  1. 01
    Not audited and not on mainnet. Do not use it with real funds yet.
  2. 02
    No TypeScript SDK or dashboard yet.
  3. 03
    No payments to GitHub repositories before their maintainers claim them.
  4. 04
    No per-split deposit address, so plain payments such as x402 cannot route into a split directly.
  5. 05
    One contract custodies every split's funds.
  6. 06
    Split loops (A pays B, B pays A) are allowed. Funds cannot be lost, but they can circulate.
  7. 07
    If a token over-reports a transfer, any excess beyond the requested amount stays in the contract.
  8. 08
    Anyone can trigger a payout to a recipient, even one that would rather wait.
11

Roadmap

Mapped to the three Stellar Community Fund build milestones.

Tranche 1in hand

MVP

  • Core contract: create, update with epochs, lock, ownership, deposit, pull, collect
  • 15 unit tests under mainnet limits, plus a cost report
  • Testnet deployment with end-to-end verification
  • TypeScript bindings and SDK
  • Property and fuzz tests for accounting invariants
Tranche 2

Testnet

  • Repository payees: fund a GitHub repo before its maintainer claims it, with a refund deadline and a swappable attestation verifier
  • Import dependency weights from PG Atlas
  • Per-split deposit addresses, so x402 and plain payments route into a split
  • Indexer and dashboard for splits, flows and claimable balances
Tranche 3

Mainnet

  • Security audit through the Soroban Audit Bank
  • Mainnet deployment, documentation and monitoring
  • Payment streams built on top of splits
12

Open source

Upstream is written from scratch and released under the Apache-2.0 license. The code, tests and documentation live at github.com/Salmatcre8/upstream.

Contributions are welcome, from new tests and SDK work to the repository-payee and PG Atlas features on the roadmap. The repository's contribution guide explains how to set up the toolchain and run the tests, and its security policy explains how to report vulnerabilities privately.

Good first issues are labelled.
Tests, the TypeScript SDK, and the roadmap features above.
Open the repository