TIP-007: Routed Payments
| Field | Value |
|---|---|
| Number | TIP-007 |
| Title | Routed Payments |
| Status | Draft — commit design settled, routing open |
| Author | Jussi Rytkönen |
| Created | 2026-09-02 |
| Updated | 2026-09-24 |
Summary
A routed payment lets Alice pay Charlie when they are not friends, by passing IOUs along a route of mutual friends: Alice→Bob→Charlie. Each hop is an ordinary IOU between friends; the route as a whole must be all-or-nothing, so that no intermediary ever ends up having paid out without being paid in.
The commit mechanism is a single multi-party record that is valid only once every participant has signed it, with one ordering rule that removes the incentive to abuse it: the payer signs last. v1 is online-only — every hop must be connected while the payment is assembled — so a routed payment completes in seconds, like any other transaction. Routing is deliberately left open — see §Phase 1.
The commit mechanism is intended to be the same primitive used by TIP-002. See §Relationship to TIP-002.
Motivation
Routed payments are the feature that turns Tally from a shared debt notebook into money: without them, value only moves between people who already trust each other. They are named in README.md and Purpose as core to the idea, and are the largest unimplemented piece of the project.
The design constraint that makes this hard is Tally's own: there is no blockchain and no global ledger (see Related work). Every comparable system — Trustlines, Circles — solved multi-hop atomicity by putting the whole path in one on-chain transaction, and solved routing by publishing the trust graph. Tally has neither, by choice.
Relationship to TIP-002 — one primitive, two features
A routed payment and a circular cancellation are structurally the same operation:
| TIP-002 circular cancellation | TIP-007 routed payment | |
|---|---|---|
| Shape | A cycle (last hop closes back to the first) | A path (distinct payer and payee ends) |
| Effect on each edge | Reduces existing debt | Creates new debt |
| Needs unanimous signing | Yes | Yes |
| Needs a canonical record every participant appends | Yes | Yes |
| Every hop is between direct friends | Yes | Yes |
They differ in one respect that matters for safety:
Cancellation can only ever make you better off; a routed payment can make you worse off.
TIP-002 can therefore sign in any order. A routed payment cannot: whoever adds the final signature decides whether and when the record becomes valid, and §Who signs last shows that only one participant can safely hold that decision.
Recommendation: build a shared multi_party_record primitive — canonical digest, signature set, signing order, reservation, signed cancel, ledger sharing rules — and express both TIPs on top of it. Its signing order is "initiator signs last", which is required here and harmless for TIP-002. If TIP-002 lands first with its own ad-hoc flow, it should be rebased onto the primitive when this TIP is implemented.
Open: whether a cycle is simply a path whose last hop points back at the first (one shape, one validation rule).
Key structural properties
1. Every hop is between direct friends. As in TIP-002, no message ever has to be routed through a non-friend: each edge of the path is an existing friendship with an existing peer connection. The protocol runs on the existing signed envelope infrastructure.
2. Only the completed payment is a ledger entry. Route discovery, proposal and signing traffic are ephemeral (like sync_hello); only the fully-signed record is durable.
3. There is no double-spend to resolve, so no consensus is needed — only evidence. Either the complete signature set exists or it does not, and it is self-verifying: anyone holding it can prove validity to anyone else. "Did it commit?" is a delivery question, and §Who signs last removes anyone's reason to interfere with delivery.
4. Intermediaries end net-neutral. Bob receives an IOU from Alice and issues one to Charlie of equal value (modulo fees), so his net balance is unchanged. What he spends is credit, not money.
The record
RoutedPayment {
id: "rpay_…" // unique; part of the signed digest
legs: [ // ordered, payer → payee
{ from: A, to: B, amount: 50.10 },
{ from: B, to: C, amount: 50.05 },
{ from: C, to: D, amount: 50.00 },
]
deadline: <timestamp> // reservation lifetime; see §Deadline
note?: <string> // visible to endpoints; see §Privacy
}
The payer is legs[0].from, the payee is the last to. Participants = every pubkey appearing in any leg. Each participant signs the canonical digest of the whole object (js/crypto/canonical.js), exactly as TIP-002 signs {route, cancel_amount}.
The record is valid if and only if every participant's signature is present and verifies. There is no partial state: an incomplete signature set is not a transaction, it is a proposal.
On commit, each participant appends the identical record to their ledger and derives their own effect from their position: a participant appearing as to in leg i and from in leg i+1 applies both.
Open: whether amounts decrease along the path (fees to intermediaries, as in the original idea notes) in v1, or whether every leg is equal for now and fees come later.
Who signs last
The final signature turns a proposal into a valid record, so the last signer holds an option: commit now, commit later, or never. The rule is to give that option to the one participant who cannot profit from it.
| Participant | Effect of commit | If they signed last |
|---|---|---|
| Payer | Owes the first hop more | Withholding only cancels their own payment; delaying changes nothing about the amount. Nothing to gain. |
| Intermediary | +in, −out, net zero | Could spring the record on neighbours at a moment of their choosing. |
| Payee | Is owed more | Could hold the record and apply the payer's debt whenever it suits them. |
Rule: the payer signs last. Every other participant signs a statement that binds them only once the payer adds theirs, and the payer's decision to commit, delay or abandon changes nothing for them except how long their credit stays reserved.
This is why the design needs no referee, no hash-lock and no received-by deadline. Rules that follow from it:
- A complete record is valid whenever it arrives. Nobody can prove when the payer signed — the payer's signature is the commit — and nobody needs to, because the payer has no reason to sign late. Rejecting late records would only split ledgers: one hop applies it, the next does not.
- The payer's own reservation is released only by the payer. The first hop keeps the payer's credit reserved until it sees the commit or the payer's signed cancel — not merely until the deadline. Otherwise a payer could let the reservation lapse, spend the same credit elsewhere, then commit and overdraw their line with the first hop: the only way a late signature could pay off. Holding it costs only the payer's own credit.
- Withholding is cancelling. A payer who changes their mind after collecting signatures should send
routed_cancelso reservations are released at once instead of at the deadline.
Phase 1 — Routing (open)
Deliberately unspecified in this draft. Tally has no global graph and does not want one, so a route must be discovered by asking friends — which is the genuinely unsolved half of this feature. Online-only v1 narrows it: only currently connected peers need to be searched.
Sketch of the option space, to be worked out in a later revision or a separate TIP:
- Probing / flooding, in the shape of TIP-002's
loop_search: ask connected friends "can you reach D for 50?", forwarded with a hop limit and a de-duplicatingsearch_id. Reuses machinery TIP-002 already needs. - Recipient-side advertisement: D tells its friends it wants to be reachable; the two searches meet in the middle.
- Cached neighbourhood knowledge: each node keeps a partial view of the graph two or three hops out, refreshed on sync.
- Manual routing: the user picks a mutual friend themselves. Ugly, but a valid v0 and useful for testing the commit path in isolation.
Open sub-questions: what "optimal" means (fewest hops / greatest bottleneck capacity / lowest fee / most debt-reducing — a route that cancels existing debt rather than creating new debt increases network capacity and should probably be preferred); how much a probe leaks.
The commit design below does not depend on how the route is found, which is why it can be specified first.
Phase 2 — Propose (forward, payer → payee)
The payer builds the record and sends it along the path. Nothing is signed or reserved yet.
Message: routed_propose (ephemeral)
Carries the unsigned record. Each hop forwards it to its next hop, after checking:
- The record is well-formed and
deadlineis in the future. - Its own legs are consistent: the amount it receives ≥ the amount it sends.
- The path does not visit it twice.
- Its next hop is currently connected (online-only v1). If not, it cancels with
peer_offline. - It has capacity on both edges right now — a fast-fail check only; the binding check happens at signing.
Phase 3 — Sign (backward, payee → payer)
The payee signs first and the signatures travel back along the path, so each participant signs knowing everyone downstream already has. The payer receives the full set of other signatures last.
Message: routed_sign (ephemeral)
Carries the record plus the signatures gathered so far. Before signing, each participant verifies:
- Every signature already present verifies.
- Its outgoing leg fits within the credit its next-hop friend extends it, after existing reservations (§Reservations).
- Its incoming leg is within its own trust limit for the previous hop — it is willing to be owed this much more by that friend.
- It has not already signed or cancelled this
id, anddeadlinehas not passed.
If all pass, the participant signs, reserves, and passes routed_sign to its previous hop. Once signed, a participant cannot retract: a signature is a commitment, and only the payer — by not signing — can still abort.
Phase 4 — Commit (forward, payer → payee)
The payer verifies all signatures, checks the deadline has not passed, signs, and sends the complete record to the first hop. Each hop verifies, appends it to its ledger, releases its reservations, and forwards it to its next hop. The payee showing "received" is the user-visible end of the payment.
The complete record is a durable ledger entry, delivered through the normal outbox. If a hop drops between receiving and forwarding, delivery resumes when it reconnects — correctness does not depend on timing, only the reservations do.
Open: whether each participant returns a receipt so the payer can show "everyone has it" rather than "it committed".
Cancel
A participant that has not yet signed may refuse, and the payer may abandon the payment at any point before their own signature:
RoutedPaymentCancel {
id: "rpay_…"
by: <pubkey>
reason?: "declined" | "insufficient_capacity" | "peer_offline" | "expired" | …
signature
}
A valid signed cancel voids the id permanently for everyone who sees it: no signature may be added afterwards, and every recipient releases its reservation immediately instead of waiting out the deadline. It propagates in both directions along the path.
A payer who signs both a cancel and a commit for the same id has equivocated. The first hop is the only way in: it forwards whichever it sees first and refuses the other, so the rest of the path never sees both, and it keeps the conflicting pair as evidence. Participants only accept routed-payment messages from their neighbours on the path.
Open: whether a cancel should be a ledger entry or ephemeral. Ephemeral is cheaper and matches TIP-002's treatment of failed flows; durable makes "why did this fail?" answerable later and gives evidence against a peer who cancels constantly.
Deadline
deadline is set by the payer, short in v1 (seconds, not minutes), and has one job: it bounds how long signers hold reservations. It does not affect validity (§Who signs last).
- The payer's client refuses to sign after the deadline, and signers refuse to add signatures after it.
- At the deadline, every reservation expires except the payer's own credit at the first hop, which waits for the payer's commit or cancel.
- Open: absolute wall-clock time or relative to a signed proposal timestamp. The ledger already distrusts local
timestampand relies on the signedoriginated_at(Ledger spec §2); clock skew between friends is a real source of spurious expiry when deadlines are this short.
Residual risks
None of these give anyone a way to profit:
- Credit held until the deadline. A payer who collects signatures and neither signs nor cancels keeps intermediaries' credit reserved until the deadline. This is a nuisance with no gain, short in v1, and caused by a friend of the first hop.
- Late arrival past a limit. If the commit reaches a hop after its reservation expired, and that credit was used in the meantime, applying it can push an edge past its limit. The hop applies it anyway (§Validation rule 6) and the UI flags it. It needs a dropped connection at the worst moment, and the overshoot is bounded by the payment amount.
- Variable amounts would reopen the option. If legs ever cross currencies or fees float with time, delaying the payer's signature gains something from the price change. Multi-currency payments must revisit this section.
In-flight reservations
Without reservations, two concurrent routed payments through Bob can each individually fit inside his available credit and jointly exceed it.
Rule: signing reserves the amount on both of that participant's affected edges — outgoing capacity toward the next hop, and headroom against the previous hop — until the record commits, is cancelled, or the deadline passes (with the payer-edge exception in §Deadline).
Reserved amounts are subtracted from available_trust everywhere it is computed (js/utils/friendships.js, js/friends-helpers.js), so a second proposal validating against the same credit correctly fails the signing check.
- Sign time, not propose time. A reservation is the local half of a commitment; reserving at propose time would let a hostile proposer freeze a friend's credit with no signature cost.
- Persisted. A reload must not drop a reservation while a signature is outstanding. Reservations live alongside the outbox and are reconciled on load against the deadline and the payer-edge rule.
- Visible. Reserved credit is shown ("€50 pending in a routed payment"), otherwise available trust silently drops and looks like a bug.
- Shared with TIP-002. A cancellation and a routed payment competing for the same edge must see each other's reservations.
Open: whether the first hop can manually release a payer's held reservation (e.g. the payer vanished), accepting the overdraw risk.
Validation rules
An inbound routed_payment entry is applied only if:
- Every participant in
legshas a present, verifying signature over the canonical digest. - It arrived from the applying node's upstream neighbour on the path (or during sync with a neighbour it shares a leg with).
- No participant appears twice in the path (a self-intersecting route would multiply that person's exposure).
- Each leg amount is positive, and amounts are non-increasing along the path.
- The
idhas not already been applied, and the node has not seen a valid cancel for it. - Limits are not re-checked at apply time. Each node checked its limits when it signed; a late arrival that now exceeds a limit is applied and surfaced as a warning. Rejecting it would leave the payment committed for some participants and not others.
Ledger integration
A routed payment is the ledger's first multi-party record, and it breaks an assumption baked into the current design: entries have one from_user_id and one to_user_id, and getLedgerEntriesForPeer slices the ledger bilaterally, never forwarding third-party entries (Ledger spec §4.1).
A routed payment record must be held by participants who are not one of its two endpoints, so the spec needs:
- a definition of which participants receive which parts during sync,
- a rule for verifying a multi-party record that arrives from someone who is not its author,
- a decision on whether the whole record is shared with all participants (simple; leaks the route) or sliced per participant (see §Privacy).
Open: whether the record is stored once with a participant list, or whether each node stores a projection of it. Storing it whole is much simpler and makes the record self-verifying on replay; projections are a prerequisite for the private variant below.
Offline routed payments (future — explicitly not v1)
v1 requires every hop to be connected, because the aim is instant payments and because a payment that sits half-signed for hours ties up credit along the whole path. The payer-signs-last rule does not depend on this: a later version can let proposals and signatures travel through relays to offline hops, with longer deadlines. The cost is longer reservations and more late-arrival overshoots, not weaker safety. Nothing in the record shape needs to change.
TIP-009 is the concrete design for one part of this: an intermediary who is unreachable takes part through a binding routing budget granted in advance, kept separate from the informal trust limit.
Partly private routed payments (future — explicitly not v1)
In v1 every participant sees the whole record: the full route, every amount, and the identities of the endpoints. That is a real privacy regression relative to bilateral transactions, and worth improving later — but the obvious fix does not work.
The tension: onion routing (Sphinx, as used by Lightning) hides the route from the hops, but requires the sender to already know the full route. Lightning affords that only because it gossips the entire channel graph publicly. Tally cannot publish that graph without giving up the property that distinguishes it. Meanwhile, hop-by-hop route discovery (§Phase 1) leaks the route to participants by construction. Hiding the route from participants and discovering it without a global graph pull in opposite directions.
What is nonetheless available later:
- Per-leg signing bound by a shared root. Each participant signs only its own leg plus a binding value (the
id, or a Merkle root over all legs) and receives an inclusion proof. Hides leg contents from non-adjacent participants, but not participation: verifying unanimity requires knowing the signer set. - Leg-to-leg encryption of the payload. The
noteand the endpoints' identities are encrypted to the endpoints, so intermediaries learn only their two neighbours and their amount. Cheap, and worth doing early — arguably even in v1. - Signature aggregation (MuSig2 / FROST over the Schnorr keys already in use). Compresses unanimity to one signature, but verification still requires the key set, so it buys little privacy, adds rounds, and nonce mishandling can leak a private key. Not worth it.
- Amount correlation remains regardless: an identical amount along a path fingerprints it. Per-hop fees perturb this slightly.
A reasonable target is "intermediaries learn their neighbours, their amount, and the path length — nothing else". The v1 record shape should avoid decisions that make it impossible (notably: keep the digest structured per-leg even if everyone currently signs the whole thing).
Why not HTLC / ILP
Interledger and Lightning solve the same commit problem with a hash time-locked contract: the recipient picks a secret r and publishes H(r); each hop promises conditionally ("I pay if shown r before T"), timeouts decrease along the path, and revealing r unlocks every leg at once.
That machinery exists for parties who do not trust each other and cannot settle socially, and it needs an enforcement layer to mean anything — for Lightning the blockchain, for ILP business contracts. Tally's hops are friends with explicit credit limits, and the payer-signs-last rule gets atomicity with correct incentives from signatures alone, with no secrets to manage and no timeout ladder. What is worth taking from ILP is the discipline, and this draft takes it: prepare forward, fulfil backward, reservations on prepare, an explicit reject.
HTLCs become worth revisiting if hops stop being friends-of-friends, or if a private variant needs a hop to commit without seeing who is downstream.
Open questions
- Deadline length for online payments, and absolute vs relative time (§Deadline).
- Maximum route length. Longer routes find more routes and multiply every failure mode; 3 hops is probably the right ceiling to start.
- Fees in v1 or later, and who sets them.
- Commit receipts back to the payer (§Phase 4).
- Manual release of a payer's held reservation by the first hop (§Reservations).
- Cancel durability — ledger entry or ephemeral (§Cancel).
- Shared primitive scope — is a cycle just a path that closes (§Relationship to TIP-002)?
- Multi-currency payments — out of scope, and they break the payer-signs-last guarantee (§Residual risks).
Implementation notes
- New ephemeral peer message kinds:
routed_propose(forward),routed_sign(backward),routed_cancel(both directions). All WebRTC-only in v1. - New ledger entry type:
routed_payment— the project's first multi-party record — delivered forward through the normal outbox. - New command:
createRoutedPayment(route, amount), alongsidecreateTransactioninjs/commands/transaction.js. - Reservations belong next to the outbox in persisted state;
available_trustcomputations must subtract them. - Signature payload: the canonical digest of
{id, legs, deadline}viajs/crypto/canonical.js, signed with the existing authorship-proof machinery (js/peer/authorship.js) so a third-party-forwarded record verifies without change. - Build the
multi_party_recordhelpers first and port TIP-002 onto them, rather than duplicating the signing flow.