When you write a Proof-of-Reserves for a custodian-backed asset, life is easy. The custodian publishes a balance, you snapshot it, you hash it, you move on with your day.
Spectra is not that.
A Spectra Metavault doesn't sit on a single balance sheet. Its backing is a living portfolio — yield-bearing positions scattered across multiple chains, money parked in DeFi protocols, vested rewards that haven't been claimed yet, and tokens drifting between a Safe multisig and the vault contract itself. There is no single number anywhere that says this is what the vault is worth right now. You have to go and compute it.
That's the story this post is about: how we taught the Proof Generator System to do exactly that.
The shape of the problem
Before we get into the pipeline, it's worth pausing on why a generic proof flow simply can't represent a Spectra vault.
If you ask "what backs the vault?", the honest answer has four parts. First, there are idle tokens sitting in the vault's Safe — straightforward enough. Second, there are DeFi positions: liquidity pool shares, lent collateral, staked balances. Third — and this is where it gets interesting — there's claimable yield from Spectra's Yield Tokens, which is real value but doesn't show up in any standard balance API; it has to be read off the corresponding Principal Token contract on-chain. And fourth, there are tokens in transit, briefly resting inside the InfraVault contract.
A proof that just says "reserves: X, liabilities: Y" throws all of that away. We wanted proof that records where the money is, how it was priced, and how confident we are in the number. So Spectra got its own NAV engine, its own proof service, and its own home in the database.
The pipeline, end to end
Here's what happens, top to bottom, every time a Spectra proof gets generated:

Every box matters, but the one that does the real work is the NAV engine in the middle. Everything upstream of it is plumbing; everything downstream is consequences. So let's open it up.
Computing NAV: a five-step walk
The NAV engine is built like a layer cake. Each step adds a piece of the picture, and the final number falls out at the top.
Step 1 — Anchor on-chain
The wrapper contract is our starting point. From it, we resolve the InfraVault address and the Safe multisig that owns everything underneath.
Once we know where to look, we read the InfraVault's reported figures: total assets, total supply, whether the vault is currently open, and any pending deposits or redeems sitting in the queue. The total-assets number gets a special label — we call it the reference NAV. It's the on-chain ground truth, and we'll come back to compare our independently-computed value against it at the very end.
Step 2 — Aggregate the portfolio via DeBank
The Safe holds the actual portfolio, scattered across chains and protocols. Reading every position by hand would mean dozens of bespoke contract calls. Instead, we lean on DeBank.
DeBank gives us the cross-chain view in a handful of API calls:
- Idle tokens held in the Safe wallet
- Liquidity pool positions
- Lending balances
- Staked or vested assets
- A spot price for every token it returns
DeBank is occasionally flaky on cold lookups — sometimes it returns partial data — so every call retries with a short timeout. Most of the time, though, it just works, and we get a complete picture in seconds rather than minutes.
Step 3 — Second-guess the prices via CoinGecko
DeBank's pricing is fine for blue chips and sometimes shaky for long-tail tokens. Anything off the beaten path is liable to be quoted low.
So we don't trust it blindly. For every token DeBank surfaces, we cross-check against CoinGecko. If CoinGecko has a higher-confidence number, we substitute it in — and we log the swap to a priceCorrections field on the result. That way, anyone replaying the proof later can see exactly which prices we accepted and which we overrode, and why.
Step 4 — Capture Spectra-specific claimable yield
This is the part no off-the-shelf API can give us. Spectra YT (Yield Token) positions accrue real value that is claimable on-chain but invisible to every balance aggregator we've tried.
To capture it, the engine does the following:
- Walks the DeBank protocol payload and plucks out every YT in the Safe
- Follows each YT on-chain to its corresponding PT (Principal Token) contract
- Reads the user's currently accrued yield directly from the PT
- Price the resulting Interest-Bearing Token amount
- Sums everything into a single claimable-yield total, denominated in the vault's base token
There's also a deduplication step in here. If a YT happens to sit inside a liquidity pool position that DeBank already counted, we make sure we don't add its yield twice.
Step 5 — Add it all up and check the deviation
Now we have everything we need. Verified NAV is just:
Safe holdings + InfraVault balance + claimable yield
All denominated in the vault's base token.
We line that up against the on-chain reference NAV from Step 1, compute the percentage deviation, and tag the result with a status:
ACCEPTED— deviation is 0.5% or lessWARNING— deviation is between 0.5% and 1.5%CRITICAL— deviation is greater than 1.5%
A CRITICAL status propagates downstream and ultimately blocks proof acceptance.
The reason for keeping two NAVs at all — verified and reference — is the whole point of the exercise. If our independently-computed value ever disagrees meaningfully with what the contract claims, something is wrong, and we want to hear about it loudly, before money moves.
From NAV to a proof-shaped object
The proof API doesn't care about all this richness. It wants two numbers: total reserves and total liabilities.
So the per-asset handler does a small reduction. Reserves become the verified NAV in vault-token units — Safe holdings plus InfraVault balance plus claimable yield. Liabilities become the outstanding share supply of the vault. The collateral ratio falls out as the first divided by the second.
What's worth highlighting: reserves are sourced from our verified holdings, not from the InfraVault's reported total assets. If the contract ever drifts from reality — a stuck position, a stale accounting state, a price oracle gone wrong — the proof reflects what's actually there, not what the contract claims is there. That's the difference between a proof you can trust and a proof that just rubber-stamps the contract.
Two gates before a proof is accepted
Computing NAV is only half the job. Before a proof is allowed to graduate from "computed" to "accepted", it has to pass two independent sanity checks:
.webp)
The collateral gate is the dumb-but-essential one. The vault's reserves must cover its share supply. If the ratio drops below 100%, something is seriously wrong — depeg, exploit, accounting bug — and we don't want a fresh proof certifying that broken state. So we PAUSE.
The volatility gate is more interesting. It looks at how much the share price has moved since the last proof and asks whether the move is plausible given the vault's history. The answer to "what's plausible" depends on how many proofs we've already collected. The very first proof skips the gate entirely — we have nothing to compare against. With one to nine proofs in hand, we use seeded defaults tuned to the asset class (stablecoins should barely budge; DeFi mixes can swing more). Between ten and twenty-seven proofs, we compute mean and standard deviation from history, but multiply the threshold by 1.5× to stay cautious while the sample is still thin. Past twenty-eight proofs, we trust the statistics straight. A move that lands inside the safe band passes; one in the warning band trips a Slack ping but still passes; anything beyond that PAUSES.
The two gates are independent. Either one can stop a proof on its own, and the NAV engine itself can also raise a CRITICAL flag from stage five. A WARNING is loud — Slack ping, database record — but the proof goes through. A PAUSE from anywhere stops the show.
What we keep, and how
Once a proof is accepted (or rejected — we keep both), it's persisted to a dedicated table designed around one principle: an auditor should be able to come back months later and reconstruct what we saw.
So we don't just store the proof blob. We store the base token price, the vault token symbol, the NAV, the total supply, the deviation percentage, the acceptance status. We store the full topology — every relevant address. And critically, we store the distribution: a per-protocol and per-token breakdown of where the reserves were sitting, including the prices we used to value each one. Plus the per-chain allocation, so you can answer "how much of the vault was on Base last Tuesday" without re-running anything.
This is what makes the proof replayable. The cryptographic proof says "here's a number, and here's why you can trust I didn't fabricate it". The metadata around it says "and here's how I arrived at the number, line by line". Together, they make the proof not just verifiable but auditable.
Updating the on-chain oracle
A proof that lives only in our database isn't doing its job. The whole point of generating one is so the rest of the system — including on-chain consumers — can act on a fresh, trustworthy NAV.
So once a proof clears the gates, we push the NAV to a per-asset oracle contract on-chain. Two values go up: the NAV expressed in USD, and the NAV per share. Both get normalised to 18 decimals on the way in, and we tag them with the source timestamp from when the NAV was computed. There's a staleness check first — if the oracle has already been updated with a newer NAV (which can happen if a previous run is still in flight somewhere), we skip the write rather than overwriting fresher data with stale data.
The signing key for these transactions doesn't live in the source. It's pulled from a secrets manager at startup, used in memory, and never touches disk. Each supported asset has its own oracle address, and that's pinned in configuration.
When things break
The pipeline is built assuming things will occasionally go wrong, because they do.
When the proof API is unreachable, we retry with exponential backoff, and only escalate to Slack if every retry fails. When DeBank returns partial data, we retry briefly and, if the result is still incomplete, populate the warnings array on the NAV result, which lets the gates make an informed decision about whether to flip the proof to WARNING or CRITICAL. When an oracle transaction gets stuck on-chain, we time out cleanly: the proof is still persisted to the database (so the audit trail isn't lost), and the next run picks up the oracle update on its next pass.
The general philosophy: never lose data, fail loudly, and let the gates decide when to stop the world.
Wrapping up
What the Spectra integration trades, compared to a vanilla Proof-of-Reserves, is simplicity. What it gets in return is verifiability. Instead of trusting one number from one source, every component of NAV is independently fetched, priced, and recorded. Every proof passes through a hard collateral floor and a statistical sanity check before it's allowed to reach an on-chain oracle. And every accepted proof carries with it enough metadata to be replayed by someone who wasn't there when it was generated.
Discover Spectra: https://www.spectra.finance

%202.png)