A gardener in Crawford Bay grows more tomatoes than her family can eat. Two streets over, someone would gladly take them. The food exists, the want exists, and no easy path connects them. The missing piece is not generosity. It is coordination.
GrowOperative is a local food app built for that problem. Neighbours list what they have and ask for what they want. Each exchange records as a running tab between two people, denominated in ordinary Canadian dollars. It is closer to a tab at a shop that knows you than to a new currency. You deal only with people you already know and trust; the network reaches through friends of friends and never places you in a transaction with a stranger.
That product is live. A soft launch runs across the Kootenays in British Columbia, with members in Crawford Bay, Nelson, Kaslo, and Fruitvale, on the iOS App Store and in Android closed testing. Nobody buys a token, manages a wallet, or watches an exchange rate. Balances stay in dollars.
The human layer stays simple. "Settle with whoever you know" is a folk concept everyone understands. The engineering is where it gets interesting, because making that folk concept correct is hard. The problem is not the tab itself. It is routing a payment atomically through a chain of people who each know only their neighbours, cancelling circular debts without anyone noticing, and keeping the system portable across a backend nobody has chosen yet. This article goes under the hood of that architecture and stays honest about which parts run today and which are still being built.
A tab, not a ledger entry
The primitive is the trustline: a credit line between two people inside one currency network. Not a pool, not a shared wallet. A single record that says how much these two neighbours currently owe each other and how far each is willing to let the other run a tab.
Two design decisions sit inside that record.
First, the trustline is stored once per pair. The two users are sorted by ID, so the trustline between Ana and Ben is always keyed user_a_id < user_b_id, never duplicated as a mirror-image row. One pair, one record, one balance. The current_balance in the app is a materialized cache; the source of truth is an append-only log, never edited in place, where a reversal is a new row, not a deletion. That discipline matters later, when the protocol gets event-sourced.
Second, the two credit limits are independent. credit_limit_a_to_b and credit_limit_b_to_a are set separately. How much I will let you owe me is my decision; how much you will let me owe you is yours. Trust is not symmetric, and the data model does not pretend it is.
The same record carries an allow_routing flag, default on. It controls whether this trustline can be a stepping stone in someone else's multi-hop payment. A member happy to keep a tab with a friend but unwilling to route other people's value through it can turn routing off without leaving the network.
Confirmation is the point worth pausing on. GrowOperative descends from the Trustlines Protocol, an open mutual-credit system that has run on an Ethereum sidechain for years, and the trustline math aligns with it on purpose. On one point the FOAF protocol diverges. In Trustlines, a payment is sender-unilateral: I can push credit onto your tab and you find out afterward. In FOAF, it is bilateral. The sender initiates a PendingPayment; the receiver confirms, rejects, or counter-proposes; the balance moves only on confirmation.
That flow is slower, and the right one for this product. An unexpected credit push is a surprise, and surprises erode trust between neighbours. Reducing the credit you extend stays unilateral and immediate, because trusting someone less is always safe to do alone. Increasing it needs a request and an acceptance. The asymmetry is the point.
Reaching past your own circle
A tab between two people who know each other is easy. The interesting case is when sender and receiver have no direct trustline at all. Ana wants to settle something with Cas, whom she does not know. But Ana trusts Ben, and Ben trusts Cas. The value should flow Ana to Ben to Cas, leaving Ben no richer and no poorer, so Ana owes Ben a little more and Ben owes Cas a little more. That is "friends of friends" as a mechanism, not a slogan.
Finding the route is a graph search. A pathfinder runs a breadth-first search over the trust graph for the shortest chain of trustlines from sender to receiver where every edge has enough remaining credit to carry the amount and none has opted out via allow_routing. The search runs off-ledger: it reads the graph, never touches balances, and returns an ordered list of hops. It plays the same role the relay server plays in Trustlines, and stays off-ledger whatever the backend becomes.
The hard part is not finding the path. It is committing it. A multi-hop payment changes several trustline balances at once, and either all of them move or none of them do. A chain that updates Ana-to-Ben but fails before it updates Ben-to-Cas has invented money on Ben's tab and lost it on Cas's. So the transfer is one atomic operation, and the execution is a fixed five steps:
1. Validate the path and verify capacity at each edge.
2. Lock every trustline in the path, in canonical ID order, with SELECT ... FOR UPDATE.
3. Create one transaction record per hop, all tagged with a shared multi_hop_id.
4. Update every balance, and commit the whole set in one database transaction.
5. On any failure at any step, roll the entire thing back.
Step 2 earns its place. Two multi-hop payments running at the same time can share trustlines. If payment A locks trustlines in the order it happens to visit them, say [7, 12], and payment B locks its overlapping set in its own order, [12, 7], then A holds 7 and waits for 12 while B holds 12 and waits for 7. Neither ever lets go. That is a deadlock, and under real concurrency it is not rare. Sorting every lock acquisition by trustline ID before taking any lock removes it: every operation that needs both 7 and 12 takes 7 first, so two operations can never hold the pieces of each other's next step. A small rule with a large payoff: the difference between a demo and something many people can use at once.
The shared multi_hop_id on every hop's record makes the payment auditable as one unit: ask the log for that ID and the whole chain comes back, intermediaries included, each net-zero as intended.
Debts that cancel themselves
Run a network of tabs for a while and the balances start to tangle. Ana owes Ben, Ben owes Cas, Cas owes Ana. Three debts, going in a circle. Each person could pay the next and everyone would end up exactly where they started, so nobody needs to pay at all. The circle can be cancelled.
A credloop service finds those circles, running as a scheduled background job that walks the signed debt graph. Like the pathfinder, it is pure computation that never touches the ledger. Finding the loop is just math, and any algorithm that returns a valid cycle is acceptable, which is why this part deliberately has no formal specification.
What it produces is a settlement that reuses the exact primitive from the previous section: a multi-hop transfer with zero net payment, run around the cycle. The amount pushed through the loop equals the smallest balance on it, the weakest link, so every debt in the circle shrinks by that much and nobody's tab moves the wrong way. Same atomic commit, same canonical lock order, same rollback on failure.
Credloop cancellation is not a separate engine. It is the multi-hop primitive pointed at a cycle instead of a payment.
The network drifts toward its own equilibrium. Tabs that would otherwise compound get quietly unwound, no cash changes hands, and the only sign is a smaller number the next time you look.
Which raises an honest question the design has not settled: when the system cancels a loop you are part of, what should you see? A notification? An opt-in? Nothing? All three are defensible. It is a product decision, not an architectural one, so it is being left open rather than guessed at. In Phase 1, with fees at zero, credloop runs as background cleanup. It earns a heavier role later: once fees can be lent along a trust path, those fee debts accumulate, and periodic loop cancellation keeps them from piling up.
Designing for a port you may never make
Everything above runs on Ruby and MySQL today. It might not always. One of the foundation's stated goals is that the same protocol could later run on a different backend: continued MySQL, a Trustlines-compatible deployment on Gnosis, or an implementation on Radix in its Scrypto language. The destination is not chosen, and might never be. The discipline is to build now as if it could be, and that has a few concrete parts.
The mathematical core is isolated from Rails. A protocol/ layer holds the pure functions: balance math, the trustline update state machine, multi-hop execution given a path. Pure values in, pure values out, with zero ActiveRecord and zero Rails imports. The core could be lifted out of the Rails engine without untangling it from the framework. Everything that touches the database, every lock and transaction and query, lives in a service layer that calls into that pure core. The math and the storage do not bleed into each other.
Then the core gets pinned down with reference specifications. For each stateless primitive, apply_direct_transfer, apply_trustline_update, apply_multi_hop_transfer, apply_settlement_state_transition, there is a language-agnostic spec in a specs/ directory: prose, pseudocode, the invariants the operation must preserve, and a set of test vectors, concrete (inputs, expected_outputs) pairs that any correct implementation has to satisfy. The Ruby implementation is verified against those vectors; a future Rust or Scrypto implementation would be verified against the same ones. Changing the backend stops being "reverse-engineer the Ruby" and becomes "implement against a known specification and pass its tests." The graph algorithms, pathfinding and credloop detection, are left out on purpose, because they are off-ledger and any valid answer is fine. Only the ledger math gets a spec.
Underneath both sits one event-sourced operations table, shared across every module. Every state change in the protocol (a payment, a trustline update, a multi-hop transfer, a settlement) is recorded there: module_name, operation_type, the inputs as JSON, actor_user_id, an idempotency_key, a timestamp, the multi_hop_id that groups related operations. State like current_balance becomes a derived cache. Replay the operations in order and you can reconstruct it. A nightly invariant check does exactly that, confirming the materialized balances still match what the log says. The same table is the migration primitive: if the backend ever changes, replay the log against the new one and check that the derived state comes out identical.
Two columns on that table are reserved for a future that may not arrive: nonce and signature. They are nullable and unused now, because in Phase 1 the backend is trusted and signs nothing. They exist anyway, because adding them later means rewriting every operation record, and reserving them now costs almost nothing. They mark out a four-phase path: today the backend is trusted; later it signs each operation with server-held keys, users none the wiser; later still those signatures mirror on-chain events and a user can eject to self-custody; and at the end signatures are client-side and mandatory, with Rails shrunk to an indexer. Only the first phase is being built. The columns are a bet that the option is worth keeping open, made while it is still cheap.
All of this traces back to the Trustlines Protocol, and the alignment is deliberate. The balance math, the update request and accept state machine, the multi-hop execution, the cycle-cancellation construction: all kept close to Trustlines' audited Solidity so the reference specs can lean on work that has already been checked in production. Where FOAF diverges, it diverges for a stated reason, not by accident:
| Decision | FOAF protocol | Trustlines Protocol | Why |
|---|---|---|---|
| Payment confirmation | Bilateral, via PendingPayment | Sender-unilateral | An unwanted credit push is a surprise; cooperative norms want consent |
| Intermediary routing | Opt-out via allow_routing | Always automatic | Member control without killing the network effect |
| Architecture | Core plus optional modules | One monolithic currency network | A deployment mounts only the modules it needs |
| Backend | Backend-agnostic, MySQL first | Ethereum sidechain, Solidity | Keep the option open while keeping the semantics compatible |
What is actually live, and what is being built
It is worth being plain about the seam between what is shipped and what is designed, because most of this article describes the second kind.
What is live is the proof of concept. The app is in people's hands across the Kootenays, on iOS and in Android closed testing, recording real tabs in Canadian dollars between real neighbours. That version is honest about its own limits: the protocol is still coupled to the app, there are no concurrency locks, credit updates are still unilateral, operations are unsigned. It works, and it is a soft launch, not a finished system.
What is being built is the architecture this article describes. Event sourcing, the bilateral confirmation flow, canonical lock ordering, the atomic multi-hop primitive, the credloop service, the extracted reference specs, the standalone trustline engine: that is Phase 1, under construction now, not yet in production. The honest framing is "here is the architecture we are building and why we are building it this way," not "here is what is running."
Two open questions are named rather than waved away. Credloop UX is undecided, as noted. Operating a custodial system that holds balances on people's behalf has regulatory implications that vary by jurisdiction; any backend move needs legal review before it happens, not after. Fees stay at zero through Phase 1; the columns and parameters for them exist but sit dormant. The FOAF Foundation has been at this since 2018, its governance token anchored on Ethereum that December. Seven years is a real arc. But the hardened protocol is recent work, and pretending otherwise would not survive contact with the codebase.
The protocol is the product
GrowOperative is the first application built on the FOAF protocol, and the foundation is candid that it is the on-ramp rather than the destination. The trustline, the multi-hop router, the credloop service, and the portability discipline are not features of a food app. They are infrastructure, and the food app is the first thing standing on them. The modules are separable: a future deployment could mount only the trustline module, or a supply-chain module, or both, the way GrowOperative eventually will.
That is the bet, and it is worth stating without inflation. Right now there is one app, one region, one food domain, and a proof of concept in soft launch. The protocol-first architecture is a wager that getting the substrate right lets other things stand on it later. Maybe they will and maybe they won't.
But the bet is visible in the decisions, not just the pitch. Bilateral confirmation instead of a unilateral push. Canonical lock ordering instead of hoping two payments never collide. One event-sourced operations table instead of mutable balances treated as truth. Reference specs with test vectors instead of behaviour you can only learn by reading the Ruby. Each of those is a small, checkable choice that makes sense mainly if you take seriously the idea that this code might outlive its first backend. Whether the port ever happens, the protocol is more honest for having been designed as though it might.