Track: Renewable Energy & Energy Trading
Inspiration
Across Africa, most new solar mini-grid connections are paid for by results-based financing: a government or donor pays the developer a fixed grant for each household connected, once the connection is verified. Nigeria's Electrification Project paid USD 350 per connection after 90 days of service; the SEforALL Universal Energy Facility pays USD 592.
Then we read how that verification actually works. The terms of reference for the verifier on Nigeria's DARES programme tell it to build its own dashboard and check it against three other tallies: the developer platform, the grants administrator's tool and the finance unit's numbers. Its recommendations "are not binding". A study of thirteen such programmes found that only 14% of USD 9.1 billion committed to African mini-grids had been disbursed by early 2025, and asked for "predictable, on-time disbursement".
So the developer builds the site, pays for panels, batteries and meters out of working capital, and waits months while four offices reconcile spreadsheets. Nobody outside can rebuild the totals. That looked like a problem a public ledger is actually for: not a token, but one record that pays by a rule nobody controls.
What it does
OpenTally puts one results-based financing programme on a public ledger.
- The funder deposits the grant pool into an escrow contract.
- Each site's metering gateway signs a monthly summary for every connection (days active, kWh, and a hash of the 30 daily readings). The contract checks every signature against that site's key; one bad signature rejects the whole batch.
- A screen compares each connection with the rest of its site and holds the ones that look wrong. It can only hold. The verifier lifts or keeps the hold, and the lift is recorded on-chain.
- When the rule is met and the sampling window has closed without a hold, anyone can trigger the release. The contract pays every eligible connection's grant to its site in one transaction and records each payment.
- Anyone can re-sum the totals from those records, in their own browser, straight from the chain.
A connection $i$ is paid for month $t$ when
$$ d_{i,t} \ge 30, \qquad k_{i,t} \ge 2.0\ \text{kWh}, \qquad \text{not held}, \qquad \tau \ge T_t + w $$
where $d_{i,t}$ is days active, $k_{i,t}$ is the month's consumption, $T_t$ is when the month's signed batch was recorded, and $w$ is the sampling window (14 days in the DARES terms, 30 seconds on the demo). The consumption threshold is ours: the terms ask for one without giving a value, and the app labels it as chosen.
On the demo, one button runs month 4 for 50 synthetic connections across three fictional sites in Kano, Kaduna and Jigawa: 50 summaries verified on-chain (one refused first because it was signed with another site's key), 3 held (two identical meters and one flat one), 47 released in one transaction, 16,450 of 17,500 test dollars paid. Open a held connection, compare its trace with the site's, lift the hold, and the same contract pays it: 48 of 50. Every visitor gets a fresh programme from a factory contract, so each judge runs it from the start.
What is real and what is not
Real, on the Base Sepolia test network: the contracts, the signatures, the rule, the holds and lifts, the release and its records. Synthetic and labelled on screen: the 50 connections, their readings and the three planted faults. The demo server holds every key so nobody needs a wallet; a real programme gives each key to its holder. Site visits and phone sampling are not built.
Why it matters for the climate
A developer paid in about a month instead of three to six puts the same money into the next site sooner, and every household connected sooner stops burning kerosene or running a diesel generator sooner. For one programme window of $N = 14{,}000$ connections (the UEF's first wave), at $e = 0.26$ tCO₂ avoided per household per year (a published IRENA case figure), with $m$ months brought forward and a share $s$ of the programme where working capital, not the size of the fund, limits building:
$$ \Delta E = N \cdot e \cdot \frac{m}{12} \cdot s = 14{,}000 \times 0.26 \times \frac{3}{12} \times 0.5 \approx 455\ \text{tCO}_2 $$
That is the central case; the high case ($m = 5$, $s = 1$) is about 1,517 tCO₂. The low case is zero on purpose: the DARES status report says demand "consistently outpaced the available financing envelope", and where the fund's size is the limit, paying faster changes cash flow and the record, not the number of connections. It is avoided emissions brought forward, a timing gain, and we say so.
How we built it
- Contracts: Solidity 0.8.28 with OpenZeppelin 5.6 (SafeERC20, EIP712, ECDSA), tested with Hardhat 3, viem and node:test.
OpenTallyholds the escrow, the site and connection registry, the rule, holds and lifts, and the release;OpenTallyFactorycreates a programme per visitor;TestUSDis a 6-decimal test token. Release marks each connection paid before any transfer and reverts if the escrow is short. - Signatures: summaries are EIP-712 typed data bound to one programme and one chain (
OpenTally / 1 / chainId / verifyingContract), so a summary cannot be replayed on another instance or network. - The screen: plain statistics, no model, so every hold can be explained. Two exact checks (identical day-by-day traces within a site; no usage on 27 or more of 30 days) and a robust z-score on each connection's monthly total against its site:
$$ z_i = \frac{0.6745\,(x_i - \tilde{x})}{\operatorname{MAD}(x)}, \qquad \text{hold if } |z_i| > 3.5 $$
where $\tilde{x}$ is the site's median monthly total and $\operatorname{MAD}$ its median absolute deviation.
- Web app: Next.js 16, React 19 and Tailwind 4 on Vercel. The month's run streams five named steps; the connection page draws the trace against the site's middle band; the re-sum drawer reads the
Releasedevents and the contract's counters from a public RPC endpoint in the browser and offers JSON and CSV. Demo keys stay on the server, and a build step scans the client bundle for any key. - Chain: Base Sepolia, with source verified on Sourcify and Blockscout. A monthly summary costs about 83,000 gas per connection and the release about 20,000.
- Tests: 10 contract tests (wrong key reverts the batch, window enforced, paid once, holds win, anyone can release), 29 unit tests and 12 browser tests against a production build.
We used Claude Code as the coding agent throughout.
Challenges we ran into
- A release with no trusted caller that the verifier can still stop. We made release permissionless after the window, and gave the verifier a hold that the contract honours, instead of a recommendation someone has to act on.
- One demo, many judges. The first judge's run would leave everyone after them looking at a finished programme, so a factory seeds spare programmes in the background and each visitor gets a fresh one.
- Sepolia stopped fitting the contract. Its current code pricing estimated our factory at 37.8M gas, above the 16.78M per-transaction cap, so we moved to Base Sepolia, where it costs 5.3M.
- Free public RPC endpoints are load-balanced. Two calls in a row can land on nodes a block apart. A gas estimate for a call to a contract created a moment earlier came back as a plain transfer, so the transaction was mined and reverted out of gas, leaving half-seeded programmes. Log queries were capped at 200 blocks on one endpoint. We pinned reads and gas estimates to the block of our latest write, sent transactions straight to Base's sequencer, split event reads into ranges, put fees in fixed steps so a nonce collision is refused instead of replacing a transaction, and made the page follow a run on-chain if its live stream drops.
Accomplishments that we're proud of
- A release any judge can run in under a minute. One button, no account, no wallet: signed summaries, a refused wrong key, three holds and 47 grants paid in one transaction on a public testnet. It ran three times in a row on the live site, each on a fresh programme.
- The security moment is real, not narrated. The contract itself refuses the summary signed with another site's key, on every run.
- Totals nobody has to take on trust. The re-sum reads the release records straight from the chain in the visitor's browser and matches them against the contract's counters, without our server in the loop.
- A verifier with teeth. A hold stops a payment by contract, a lift is recorded on the ledger, and the lifted connection is paid by the same rule.
- Source and tests in the open. Contracts verified on Sourcify and Blockscout; 10 contract tests, 29 unit tests and 12 browser tests.
- A climate number with its working shown, including the case where it is zero.
What we learned
- The hard part of results-based financing is not moving money. It is agreeing on the record. Once the record is public and the rule is code, the payment is the easy step.
- A screen that can only hold is much easier to defend than one that decides. Judges, verifiers and developers can all see why a connection waits.
- "Read your own write" is not free on a public chain. A serious app has to treat every node as possibly a block behind.
- Honest limits make a stronger case than big numbers. The climate projection includes a zero case because the evidence says the fund's size often binds first.
What's next for OpenTally
A shadow-mode pilot on one developer's real meter export, run beside the existing verification process; paged releases and one Merkle root per site per month, so a programme of tens of thousands of connections fits in ordinary blocks; and emissions per connection computed from each month's signed kWh.
Log in or sign up for Devpost to join the conversation.