Inspiration

Companies that call hosted AI models are starting to get one question from customers: what does this cost the climate? Today the honest answer is a number in a spreadsheet that the vendor wrote and can change. Picture selling an AI feature to a hospital whose procurement team asks for proof. You have an estimate, and they have no way to tell a careful one from a convenient one.

We wanted to see what it would take to make that number checkable, without pretending it is more precise than it is.

What it does

Green Receipts sits in front of a model API as a proxy. For each request it estimates the energy as a range, applies the carbon intensity of the grid in the region the caller declares, adds the embodied emissions of the hardware, and returns a receipt. The estimate has the same shape as the Software Carbon Intensity formula, with one request as the unit:

$$ \text{gCO}_2\text{e} = E \times I + M $$

Here \( E \) is energy in kWh, \( I \) is grid intensity in gCO2e per kWh, and \( M \) is embodied emissions.

The receipt shows the range, the method, where the grid data came from, and a plain "not audit-grade" label. The issuer signs it. A smart contract on the Sepolia testnet stores its hash and emissions range and adds the range to the organisation's running total.

Anyone can verify a receipt. The check confirms that the signature recovers a signer, that the signer is the account the chain recorded the receipt from, that this exact receipt is on the chain, and that the recorded range matches. Change one field and the receipt is rejected and stamped void. A standalone script runs the same checks straight against the chain, so you do not have to trust our server.

There is also a region comparison. It sends the same prompt to five declared regions, and on our sample data the dirtiest one costs about eight times the cleanest.

How we built it

The energy range and embodied emissions come from the open source EcoLogits library. The service is Python and FastAPI. Each receipt is turned into a canonical JSON form, hashed with keccak256 and signed with the issuer key.

The contract is Solidity with OpenZeppelin access control. It stores the hash, a hashed organisation id, a timestamp and the range in whole nanograms. Hardhat handled local development and the contract tests, and web3.py talks to the chain from the service.

The dashboard is plain HTML, CSS and JavaScript. The methodology page is generated from the same constants the receipts use, so the page and the code say the same thing. For the demo we deployed the contract to Sepolia and run the API in a Docker container on Render.

Challenges we ran into

Hashing was harder than we expected. The browser re-serialises a receipt when you edit it, and languages print numbers differently, so one receipt could hash two ways. We settled on a canonical form with sorted keys, no whitespace and plain decimal numbers, and we test the round trip.

A restarted local node wipes the chain. The service now checks on every request that the contract still exists, and it recovers after one redeploy with clear errors in between.

When we deployed to Sepolia we found that the RPC URL, which can contain a provider key, was being echoed in API responses and in downloaded receipt files. We split it into a private URL the server uses and a public one it shows.

The last challenge was restraint. The model is a mock, the grid values are an illustrative sample, and the caller declares the region. Every receipt says so, and we tried to keep the demo and this page from claiming more.

What we learned

A tamper-evident record protects a number. It does not make the number true. The choice that helped most was putting every assumption on the receipt itself, where the person reading it can see it.

Testing

There are more than 100 Python tests and 11 contract tests. An independent review found six issues, and all six are fixed and tested. Receipts stay valid after the issuer key is rotated, because each on-chain record stores who recorded it.

Limits

This is a prototype. The model is a mock with simulated latency, the grid data is a sample and not live, and the caller declares the region. There is one issuer key and no authentication. It runs on Sepolia, so nothing has real value, and each request costs one transaction. The totals are a bounding envelope of the issuer's claims, not a statistical interval. It does not issue or retire carbon credits.

What's next

Live hourly grid data, and energy and region reported by the provider instead of declared by the caller. Per-organisation keys with rate limits. Binding each receipt to a hash of the real request. Batching many hashes into one record, since a transaction per request will not scale. Then a move from Sepolia to a production network, and mapping the schema to an established reporting standard.

Built With

Share this project:

Updates

Submission history