A stablecoin’s peg is only as good as the dollars sitting behind it, and in 2026 regulators finally started forcing issuers to prove that math on a schedule. The GENIUS Act now requires monthly reserve examinations, executive sign-off from the CEO and CFO, and a full GAAP audit for any issuer holding more than $50 billion in outstanding tokens. That is a real compliance clock, not a marketing promise.
The problem is that most holders never check any of it. They see a green “backed 1:1” badge on an exchange page and move on. This tutorial walks through a hands-on way to verify stablecoin proof of reserves yourself, combining the issuer’s published attestation report with on-chain data, a Chainlink Proof of Reserve feed, and a Merkle-tree inclusion check. By the end you will have a working Python script that pulls circulating supply, compares it against reported reserves, flags a stale attestation, and fires an alert when the ratio drops below 1.0.
The point isn’t to replace the accountants doing the monthly examinations, or to second-guess a regulator’s rulemaking. It’s to close the gap between “a report exists somewhere on a legal page” and “I personally checked the numbers this week.” That gap is exactly where most retail holders currently sit, and it’s closable with free tools and a few hours of setup.
None of this requires running your own Ethereum node, a law degree, or a six-figure audit contract. It requires a laptop, a free RPC endpoint, and about 90 minutes split across 12 steps. It also fits neatly alongside the rest of our cryptocurrency security coverage, since a reserve check is really just another item on the same custody-and-counterparty checklist as wallet security and bridge risk.
Why Stablecoin Reserve Verification Matters in 2026
Stablecoin proof of reserves stopped being an optional nice-to-have the moment the GENIUS Act’s compliance stack started landing. Permitted payment stablecoin issuers now have to back every outstanding token with eligible reserve assets at a 1:1 ratio, and, per the Congressional Research Service’s breakdown of the law, a registered public accounting firm has to examine that reserve disclosure every single month. Each monthly report needs a certification signed by the issuer’s CEO and CFO, and knowingly filing a false certification carries criminal exposure, not just a fine.
Issuers crossing $50 billion in outstanding stablecoins face a higher bar still: annual GAAP financial statements audited under PCAOB standards, the same framework public companies use. The FDIC’s federal register notice on GENIUS Act requirements confirms federal and state regulators were required to finalize their implementing regulations within a year of enactment, and law firm Paul Weiss has noted the full compliance framework is scheduled to take effect on January 18, 2027. That date matters because it is the point where “attestation” language stops being enough for the largest issuers and a real audit trail becomes mandatory.
Tether and Circle, the two largest issuers, illustrate why self-verification still matters even with these rules in motion. Tether’s USDT circulation sits around $189 billion against quarterly attestations performed by BDO Italia, while Tether’s own reported reserve buffer shrank from $8.23 billion in Q1 2026 to $4.11 billion in Q2, a swing worth watching if you hold the token. Circle’s USDC, at roughly $76 billion in circulation, runs monthly attestations through Deloitte using AICPA agreed-upon procedures. Both are legitimate processes. Neither is a full financial-statement audit, and knowing the difference is the entire point of this tutorial.
A regulatory requirement also only works as well as the reader’s ability to parse what gets filed in response to it. A monthly PDF with an accounting firm’s letterhead looks authoritative whether or not anyone outside the issuer ever cross-checks the underlying numbers against the blockchain. That’s the gap this tutorial closes: not by distrusting the attestation process, but by giving you an independent, repeatable way to confirm what it says lines up with what’s actually on-chain.
What Proof of Reserves Actually Proves (and Its Limits)
“Proof of reserves” gets used as a catch-all term, but it covers at least three distinct claims, and conflating them is the single most common mistake readers make when they try to sanity-check a stablecoin on their own.
- Liabilities proof. A Merkle-tree based proof shows that a specific customer balance was included in a published liability snapshot at a point in time, nothing more.
- Assets proof. Wallet addresses, custodian statements, or a Chainlink Proof of Reserve feed independently establish what the issuer or exchange actually holds.
- Solvency proof. Solvency requires comparing a complete, verified liability set against complete, verified assets. A Merkle proof alone cannot show liabilities were complete, that assets were unencumbered, or that the snapshot wasn’t temporarily window-dressed before publication.
An attestation, in the accounting sense, is also narrower than most readers assume. It applies agreed-upon procedures to balances at one specific moment and explicitly does not examine internal controls, revenue, expenses, or the issuer’s broader financial condition, a point a recent industry analysis of Circle’s trust charter and Tether’s attestation practice makes directly. That is why Tether’s quarterly reports and Circle’s monthly reports are both described as attestations rather than audits, and why Tether reportedly engaged an unnamed Big Four firm in mid-2026 to pursue its first full independent financial-statement audit. The distinction sounds pedantic until you remember that an attestation can be clean while the business behind it is not, because the procedures were never designed to check that.
Chainlink Proof of Reserve feeds close part of that gap by bringing off-chain reserve data on-chain through a decentralized oracle network, so smart contracts and monitoring scripts can compare reported reserves against token supply without waiting for a quarterly PDF, a mechanism Spark’s research on the evolution of stablecoin proof of reserves lays out in more depth. That is the piece we will build around in the steps below, combined with raw on-chain supply checks and a Merkle inclusion verifier for exchange-style proofs.
It helps to think of the whole exercise as three separate checks running in parallel rather than one monolithic “is it backed” question. The on-chain supply check tells you how many tokens exist right now. The attestation or oracle feed tells you what the issuer claims to hold against them. The ratio between the two is the only number that actually matters for a holder, and it’s the number most coverage of stablecoins skips in favor of headline circulation figures.
Prerequisites: Tools, Accounts, and Versions
You do not need a trading account or a paid data subscription for this tutorial. You need a working Python environment and access to a public blockchain RPC endpoint. Here is the full list:
- Python 3.11 or later installed locally (check with
python3 --version) - pip, bundled with modern Python installs
- web3.py, version 7.x or later, for talking to Ethereum-compatible chains
- requests, latest version, for webhook alerts and HTTP calls
- A free public RPC endpoint (PublicNode, Infura, Alchemy, or your own node) since none of the code below requires a paid API key
- A text editor or IDE of your choice
- A webhook URL if you want live alerts (Slack incoming webhook, Discord webhook, or any endpoint that accepts a JSON POST)
- About 90 minutes, since several steps involve waiting on blockchain confirmations
Everything here runs on a free tier. You are not spinning up infrastructure, you are reading data that is already public and organizing it into a repeatable check. If you’ve ever written a small script to scrape a price feed or poll an API on a timer, the skill set overlaps almost completely. The main thing that’s new here is reading ERC-20 contract calls instead of a REST endpoint, and that turns out to be a smaller jump than it sounds.
Step 1: Identify the Issuer’s Attestation Framework
Before writing a line of code, find out what kind of report the issuer actually publishes, because “proof of reserves” means something different depending on who is issuing it. Large fiat-backed stablecoin issuers like Tether and Circle publish periodic attestation reports from a named accounting firm. Exchanges that hold customer deposits (rather than issuing a token) typically publish Merkle-tree liability proofs instead. Some newer issuers lean on Chainlink Proof of Reserve feeds as a live, on-chain supplement to their periodic reports.
Go to the issuer’s own transparency or legal page and note three things: the name of the accounting firm, the reporting frequency (monthly or quarterly), and whether the filing is labeled an “attestation” or an “audit.” Those words are legally distinct, and issuers are generally careful to use the correct one. If a report doesn’t specify which type it is, treat that as a red flag worth digging into rather than assuming the best case.
Step 2: Pull the Latest Attestation Report and Read the Opinion Letter
Download the most recent report as a PDF and go straight to the accountant’s opinion letter, usually on the first or second page. You are looking for specific language: “we have examined,” “agreed-upon procedures were applied,” or “in our opinion, the schedule presents fairly.” Each phrase carries different legal weight, and agreed-upon procedures reports (the kind both Tether and Circle currently publish) explicitly disclaim giving an opinion on the fairness of the overall financial statements.
Note the “as of” date on the report. This single date is what you will check for staleness in Step 10, since a report that was accurate on its publication date tells you very little about reserves six weeks later.
Step 3: Get the Token’s Official Contract Address On-Chain
Every verification step from here depends on using the correct, official contract address, not a copy-pasted address from a forum post or a scam token with a similar name. Confirm the address directly from the issuer’s own documentation or from a well-known block explorer, and double-check it against at least one independent source before using it in any script.
For reference, the two most widely held fiat-backed stablecoins on Ethereum use these mainnet contract addresses:
| Token | Issuer | Ethereum mainnet contract address |
|---|---|---|
| USDT | Tether | 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
| USDC | Circle | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
Both tokens are also issued on multiple other chains, which matters for Step 8. Treat the Ethereum address as one data point, not the whole picture.
A quick sanity check worth doing here: paste the address into a block explorer and confirm the contract is verified, has a long transaction history, and matches the symbol and name you expect. A freshly deployed contract with a handful of transactions and the same ticker as a major stablecoin is one of the oldest tricks in the book, and it takes about thirty seconds to rule out.
Step 4: Set Up Your Local Verification Environment
Create an isolated environment so this project’s dependencies don’t collide with anything else on your machine.
python3 -m venv por-venv
source por-venv/bin/activate
pip install web3 requests
python3 --version
On Windows, replace the activation line with por-venv\Scripts\activate. Confirm web3.py installed correctly by running python3 -c "import web3; print(web3.__version__)" before moving on.
Step 5: Reconcile Reported Reserves Against On-Chain Circulating Supply
The attestation report tells you what the issuer says it holds. The blockchain tells you how many tokens actually exist. Those two numbers should move together, and checking that is the first real verification step.
from web3 import Web3
RPC_URL = "https://ethereum-rpc.publicnode.com"
web3 = Web3(Web3.HTTPProvider(RPC_URL))
ERC20_ABI = [
{
"constant": True,
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function",
},
{
"constant": True,
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"type": "function",
},
]
TOKENS = {
"USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
}
for symbol, address in TOKENS.items():
contract = web3.eth.contract(
address=Web3.to_checksum_address(address), abi=ERC20_ABI
)
raw_supply = contract.functions.totalSupply().call()
decimals = contract.functions.decimals().call()
supply = raw_supply / (10 ** decimals)
print(f"{symbol} on-chain circulating supply (this chain only): {supply:,.2f}")
Example output (your figures will differ since supply changes block by block):
USDT on-chain circulating supply (this chain only): 94,112,006,558.19
USDC on-chain circulating supply (this chain only): 41,874,220,117.03
Those per-chain numbers will be lower than the headline circulation figures reported in attestations, because both tokens are minted across several chains. Keep that number handy for Step 8, where you combine chains into one total.
Step 6: Query a Chainlink Proof of Reserve Feed
Chainlink Proof of Reserve feeds expose a live, on-chain figure for reported reserves, updated by a decentralized oracle network rather than a single party. Look up the correct feed address for your target asset in Chainlink’s own data feeds documentation at data.chain.link before running this, since feed addresses vary by asset and by network and using the wrong one will simply return the wrong number silently.
POR_ABI = [
{
"inputs": [],
"name": "latestRoundData",
"outputs": [
{"name": "roundId", "type": "uint80"},
{"name": "answer", "type": "int256"},
{"name": "startedAt", "type": "uint256"},
{"name": "updatedAt", "type": "uint256"},
{"name": "answeredInRound", "type": "uint80"},
],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"stateMutability": "view",
"type": "function",
},
]
# Look up the correct feed address for your asset at data.chain.link
POR_FEED_ADDRESS = "0xYOUR_FEED_ADDRESS_HERE"
por_contract = web3.eth.contract(
address=Web3.to_checksum_address(POR_FEED_ADDRESS), abi=POR_ABI
)
round_data = por_contract.functions.latestRoundData().call()
feed_decimals = por_contract.functions.decimals().call()
reported_reserves = round_data[1] / (10 ** feed_decimals)
last_updated = round_data[3]
print(f"Chainlink-reported reserves: {reported_reserves:,.2f}")
print(f"Feed last updated (unix timestamp): {last_updated}")
If answer comes back as zero or the call reverts entirely, that almost always means the feed address, network, or asset pairing is wrong, not that reserves actually dropped to zero. Troubleshooting that failure mode is covered in the troubleshooting section below.
Step 7: Compare Feed Data to Circulating Supply and Compute the Ratio
With both numbers in hand, a reserve ratio is a one-line calculation. The threshold to watch is 1.0, since the GENIUS Act’s 1:1 backing requirement means anything below that is a compliance problem, not just a rounding error.
def reserve_ratio(reported_reserves, circulating_supply):
if circulating_supply == 0:
raise ValueError("circulating supply cannot be zero")
return reported_reserves / circulating_supply
ratio = reserve_ratio(reported_reserves, supply)
status = "fully backed" if ratio >= 1.0 else "UNDER-COLLATERALIZED"
print(f"Reserve ratio: {ratio:.4f} ({status})")
Example output:
Reserve ratio: 1.0213 (fully backed)
A ratio slightly above 1.0, like the example above, usually reflects a reported surplus buffer rather than an error. That lines up with how Tether has described its own reserve position: a reported surplus of several billion dollars sitting above the amount needed for 1:1 backing, which the company frames as a cushion against market shocks.
Step 8: Handle Multi-Chain Issuance
Both USDT and USDC exist on more chains than just Ethereum, and a verification script that only checks one chain will understate real circulating supply, which in turn makes the reserve ratio look artificially inflated. Loop over every chain the token is issued on and sum the results before comparing against reported reserves. Multi-chain issuance also widens the attack surface on the messaging layer that keeps supply in sync across networks, which is the same class of problem covered in our cross-chain bridge security research.
CHAINS = {
"ethereum": "https://ethereum-rpc.publicnode.com",
"bsc": "https://bsc-rpc.publicnode.com",
}
total_supply = 0.0
for chain_name, rpc_url in CHAINS.items():
chain_web3 = Web3(Web3.HTTPProvider(rpc_url))
contract = chain_web3.eth.contract(
address=Web3.to_checksum_address(address), abi=ERC20_ABI
)
raw = contract.functions.totalSupply().call()
dec = contract.functions.decimals().call()
chain_supply = raw / (10 ** dec)
print(f"{chain_name}: {chain_supply:,.2f}")
total_supply += chain_supply
print(f"Combined circulating supply across checked chains: {total_supply:,.2f}")
This example checks two chains for brevity. A complete check should cover every chain listed on the issuer’s own transparency page, since omitting even one popular chain can shift the ratio by a meaningful margin for a token with tens of billions in circulation.
Step 9: Build a Merkle-Tree Inclusion Verifier for Exchange-Style Proofs
If you are checking an exchange’s proof of reserves rather than an issuer’s, the mechanism is usually a Merkle tree instead of an attestation PDF. The exchange hashes every customer’s account identifier and balance into a leaf node, repeatedly hashes pairs of nodes until one root hash remains, then publishes that root publicly along with a tool that gives each user their own inclusion path.
The reason exchanges reach for a Merkle tree instead of just publishing a spreadsheet of balances is privacy. Publishing every customer’s balance in plain text would be both a competitive and a security disaster. A Merkle tree lets the exchange prove inclusion of any single balance without exposing the rest, which is the same cryptographic trick used in plenty of other places you’ve probably run into, from certificate transparency logs to blockchain light clients.
import hashlib
def leaf_hash(account_id, balance):
data = f"{account_id}:{balance}".encode()
return hashlib.sha256(data).hexdigest()
def verify_merkle_proof(leaf, proof, published_root):
computed = leaf
for sibling_hash, position in proof:
pair = computed + sibling_hash if position == "right" else sibling_hash + computed
computed = hashlib.sha256(pair.encode()).hexdigest()
return computed == published_root
# Replace these with the real leaf, proof path, and root from the exchange's own tool
my_leaf = leaf_hash("user_12345", "50000.00")
proof_path = [
("a1b2c3d4e5f6...", "right"),
("f6e5d4c3b2a1...", "left"),
]
published_root = "9f8e7d6c5b4a..."
included = verify_merkle_proof(my_leaf, proof_path, published_root)
print(f"Account included in published liability snapshot: {included}")
Remember what this does and doesn’t show. A successful result confirms your balance was included in the published liability set at that moment. It says nothing about whether the exchange’s listed assets actually cover every included liability, which is why this step pairs with, rather than replaces, Steps 5 through 8.
Step 10: Automate Attestation Freshness Checks
Since the GENIUS Act requires monthly examinations for permitted issuers, a report that is more than about five weeks old (30 days plus a short publication grace period) is worth flagging automatically rather than noticing by accident.
from datetime import datetime, timezone
def attestation_is_stale(report_date_str, max_age_days=35):
report_date = datetime.strptime(report_date_str, "%Y-%m-%d").replace(
tzinfo=timezone.utc
)
age_days = (datetime.now(timezone.utc) - report_date).days
return age_days > max_age_days, age_days
stale, age = attestation_is_stale("2026-09-01")
print(f"Report age: {age} days | Stale: {stale}")
Swap in the “as of” date you recorded back in Step 2. For quarterly reporters, raise max_age_days to roughly 100 to match their actual cadence instead of flagging every report as stale by default. As the GENIUS Act’s monthly-examination requirement fully takes hold, expect more issuers to shift from quarterly to monthly cycles, which means this threshold is worth revisiting every few months rather than hardcoding once and forgetting about it.
Step 11: Set Up Reserve-Ratio Alerting
A script that only prints to a terminal is easy to ignore. Wiring it to a webhook turns it into something that actually taps you on the shoulder when a number moves.
import requests
WEBHOOK_URL = "https://hooks.example.com/your-webhook-id"
def send_alert(message):
try:
requests.post(WEBHOOK_URL, json={"text": message}, timeout=10)
except requests.RequestException as err:
print(f"Alert delivery failed: {err}")
if ratio < 1.0:
send_alert(f"ALERT: reserve ratio dropped to {ratio:.4f}")
elif stale:
send_alert(f"ALERT: attestation report is {age} days old")
else:
print("No alert conditions triggered.")
Replace the placeholder webhook URL with a real Slack or Discord incoming webhook, or any endpoint on your own infrastructure that accepts a JSON POST body.
Step 12: Assemble the Complete Reserve-Monitoring Script
This is the complete working project: every piece from Steps 5 through 11 combined into one script you can schedule to run on its own. Save it as reserve_monitor.py.
from datetime import datetime, timezone
from web3 import Web3
import requests
RPC_URL = "https://ethereum-rpc.publicnode.com"
WEBHOOK_URL = "https://hooks.example.com/your-webhook-id"
POR_FEED_ADDRESS = "0xYOUR_FEED_ADDRESS_HERE"
ATTESTATION_DATE = "2026-09-01"
TOKEN_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
ERC20_ABI = [
{"constant": True, "inputs": [], "name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}], "type": "function"},
{"constant": True, "inputs": [], "name": "decimals",
"outputs": [{"name": "", "type": "uint8"}], "type": "function"},
]
POR_ABI = [
{"inputs": [], "name": "latestRoundData",
"outputs": [{"name": "roundId", "type": "uint80"},
{"name": "answer", "type": "int256"},
{"name": "startedAt", "type": "uint256"},
{"name": "updatedAt", "type": "uint256"},
{"name": "answeredInRound", "type": "uint80"}],
"stateMutability": "view", "type": "function"},
{"inputs": [], "name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"stateMutability": "view", "type": "function"},
]
def get_circulating_supply(web3, token_address):
contract = web3.eth.contract(
address=Web3.to_checksum_address(token_address), abi=ERC20_ABI
)
raw = contract.functions.totalSupply().call()
dec = contract.functions.decimals().call()
return raw / (10 ** dec)
def get_reported_reserves(web3, feed_address):
contract = web3.eth.contract(
address=Web3.to_checksum_address(feed_address), abi=POR_ABI
)
round_data = contract.functions.latestRoundData().call()
dec = contract.functions.decimals().call()
return round_data[1] / (10 ** dec)
def attestation_is_stale(report_date_str, max_age_days=35):
report_date = datetime.strptime(report_date_str, "%Y-%m-%d").replace(
tzinfo=timezone.utc
)
age_days = (datetime.now(timezone.utc) - report_date).days
return age_days > max_age_days, age_days
def send_alert(message):
try:
requests.post(WEBHOOK_URL, json={"text": message}, timeout=10)
except requests.RequestException as err:
print(f"Alert delivery failed: {err}")
def main():
web3 = Web3(Web3.HTTPProvider(RPC_URL))
supply = get_circulating_supply(web3, TOKEN_ADDRESS)
reserves = get_reported_reserves(web3, POR_FEED_ADDRESS)
ratio = reserves / supply
stale, age_days = attestation_is_stale(ATTESTATION_DATE)
print(f"Circulating supply: {supply:,.2f}")
print(f"Reported reserves: {reserves:,.2f}")
print(f"Reserve ratio: {ratio:.4f}")
print(f"Attestation age: {age_days} days (stale: {stale})")
if ratio < 1.0:
send_alert(f"ALERT: reserve ratio dropped to {ratio:.4f}")
if stale:
send_alert(f"ALERT: attestation report is {age_days} days old")
if __name__ == "__main__":
main()
Schedule it with cron so it runs without you remembering to:
0 9 * * * /path/to/por-venv/bin/python3 /path/to/reserve_monitor.py >> /var/log/reserve_monitor.log 2>&1
That line runs the check once a day at 9 a.m. and appends output to a log file you can review weekly. If you’re watching more than one stablecoin, duplicate the script with a different TOKEN_ADDRESS, POR_FEED_ADDRESS, and ATTESTATION_DATE for each one, and point the alerts at distinct Slack or Discord channels so a flagged ratio on one token doesn’t get lost in noise from another. The core logic doesn’t change. Only the three configuration values at the top of the file do.
GENIUS Act Deadlines and Issuer Practices Compared
Two tables cover the regulatory side and the issuer-practice side of what you are actually checking for.
| Requirement | Detail | Deadline / Cadence |
|---|---|---|
| Reserve backing | 1:1 ratio using eligible reserve assets | Ongoing |
| Reserve disclosure examination | By a registered public accounting firm | Monthly |
| Executive certification | Signed by CEO and CFO; false certification can trigger criminal penalties | Monthly |
| Full GAAP audit under PCAOB standards | Required for issuers above $50 billion outstanding | Annual |
| Implementing regulations finalized | Federal and state regulators had to promulgate rules | By July 18, 2026 |
| Full compliance framework effective | Entire reporting stack enforceable | January 18, 2027 |
| Metric | Tether (USDT) | Circle (USDC) |
|---|---|---|
| Reported circulation (2026) | ~$189 billion | ~$76 billion |
| Attestation firm | BDO Italia | Deloitte |
| Reporting frequency | Quarterly | Monthly |
| Report type | Attestation (agreed-upon procedures) | Attestation (AICPA agreed-upon procedures) |
| Reported reserve buffer/surplus (2026) | $8.23B (Q1) to $4.11B (Q2) | Not specified in available reporting |
| Full independent financial audit | Reportedly engaged a Big Four firm, unnamed, for a first full audit | Not yet a full GAAP audit per available reporting |
Treat both tables as a snapshot rather than a permanent ranking. Reserve buffers and attestation arrangements change quarter to quarter, which is exactly why a repeatable verification process beats a one-time check.
Common Pitfalls When Verifying Stablecoin Reserves
- Treating a snapshot as proof of solvency. A single point-in-time reserve figure says nothing about the hours or days before and after it was taken.
- Confusing an attestation with an audit. Agreed-upon procedures reports explicitly do not examine internal controls or the issuer’s full financial condition, no matter how official the PDF looks.
- Trusting a Chainlink feed without checking its freshness. A feed that hasn’t updated in days is stale data wearing a live-looking interface; always read the
updatedAtfield. - Ignoring multi-chain supply. Checking only Ethereum for a token issued on five or more chains will consistently understate real circulating supply.
- Mistaking Merkle inclusion for solvency proof. Inclusion in a liability snapshot proves your balance was counted. It proves nothing about whether total assets cover total liabilities.
- Using an unverified contract address. Scam tokens routinely clone legitimate token names; always cross-check the address against the issuer’s own documentation.
- Forgetting decimals. ERC-20 tokens report raw integer values; skipping the decimals division produces a number a trillion times too large.
- Running the check once and calling it done. A single clean reading is a snapshot, not a trend. Reserve ratios, attestation dates, and even reserve asset composition shift over time, and the whole value of this exercise comes from repeating it on a schedule rather than treating it as a one-time box to tick.
Troubleshooting Guide
- “ContractLogicError” on totalSupply() calls. Usually means the ABI doesn’t match the deployed contract, or the address is for a proxy contract rather than the implementation.
- Connection timeouts to the RPC endpoint. Free public RPC providers rate-limit aggressively; add a short retry with backoff or switch to a second provider as a fallback.
- Supply numbers off by a factor of a million or more. Almost always a missed decimals() division; print the raw integer and the decimals value separately to spot it.
- Chainlink feed returns answer = 0. Check that you have the correct feed address for the correct asset on the correct network; wrong-network addresses often still return a response, just the wrong one.
- “InvalidAddress” from web3.py. The address needs checksum casing; wrap it in
Web3.to_checksum_address()before use. - Webhook alerts never arrive. Confirm the webhook URL accepts the exact JSON shape your platform expects; Slack and Discord webhooks use slightly different payload keys.
- Attestation PDF link returns a 404. Issuers frequently archive old reports under a new URL path; go to the issuer’s current transparency page rather than an old bookmark.
- Script reports a ratio near zero. Before assuming a real problem, check whether you accidentally compared one chain’s supply against a feed meant to cover all chains combined, which is a common Step 6-to-Step 8 mismatch.
Advanced Tips for Ongoing Reserve Monitoring
Once the basic script runs reliably, a few additions make it genuinely useful rather than a one-off exercise. Diff successive attestation reports against each other rather than reading each one in isolation, since a slow drift in the reserve asset mix (more commercial paper, less in short-dated Treasuries, for instance) can show up months before it becomes a headline. Run your checks against at least two independent RPC providers and compare results, since a single compromised or misconfigured provider can silently feed you wrong supply numbers.
It’s also worth building a small archive rather than overwriting the same log file forever. Store each run’s supply, reserves, ratio, and attestation age as a row in a CSV or SQLite table with a timestamp, and after a few months you’ll have your own independent time series to compare against whatever the issuer publishes. That archive becomes genuinely valuable the first time a reported figure doesn’t match what your own history shows, because at that point you have a dated, reproducible record instead of a vague memory of “it used to look different.”
Keep an eye on the $50 billion GAAP-audit threshold from the GENIUS Act table above. An issuer sitting near that line today may be filing simple attestations now and full PCAOB-standard audited financials within a year, which changes what kind of document you should expect to find on their transparency page. Finally, log every ratio and staleness check with a timestamp in a simple CSV or database rather than just printing to a terminal, since a single clean reading tells you far less than six months of consistent readings do.
If you hold large stablecoin balances yourself, pair this reserve check with your own custody hygiene. Our hardware wallet security checklist and multisig wallet setup guide cover the other half of the equation: making sure the tokens you’ve verified as properly backed are also tokens nobody else can move. Readers thinking further ahead should also look at our wallet audit for quantum risk, since reserve verification and custody risk are two separate problems that both deserve a repeatable checklist, not a one-time glance.
Frequently Asked Questions
What’s the real difference between an attestation and an audit?
An attestation applies agreed-upon procedures to a specific set of balances at one point in time and does not examine internal controls or the issuer’s overall financial condition. A full audit, performed under GAAP and PCAOB standards, is a broader examination that results in an opinion on whether financial statements are fairly presented. Both Tether and Circle currently publish attestations rather than full audits, though Tether has reportedly engaged a firm to work toward its first full audit. If you only remember one thing from this entire tutorial, make it this distinction, since it’s the one most marketing copy around stablecoins quietly glosses over.
Does the GENIUS Act require real-time, on-chain proof of reserves?
No. The Act requires a monthly examination by a registered public accounting firm along with CEO and CFO certification, not a continuous on-chain feed. Chainlink Proof of Reserve and similar tools are supplementary, issuer-adopted solutions that add a live data layer on top of the regulatory minimum, not a substitute for it.
Can a Chainlink Proof of Reserve feed alone prove a stablecoin is solvent?
No. The feed reports an asset figure sourced from off-chain data through a decentralized oracle network. It still depends on the accuracy of the underlying data source, and it says nothing on its own about total liabilities or whether those liabilities were counted completely. Pair it with a supply check and, where available, an attestation report.
How often do Tether and Circle actually publish reserve reports?
Tether publishes quarterly attestations through BDO Italia. Circle publishes monthly attestations through Deloitte using AICPA agreed-upon procedures. Both frequencies are expected to be reviewed against the GENIUS Act’s monthly-examination requirement as the compliance framework fully takes effect.
What happens if a CEO or CFO signs off on a false reserve report?
The GENIUS Act’s certification requirement attaches criminal exposure to a knowingly false certification, not just civil liability. That is a meaningfully higher bar than the reputational risk issuers faced before the Act, and it is part of why the executive-sign-off requirement is treated as one of the law’s sharper teeth.
Do I need to run my own blockchain node to do any of this?
No. Every example in this tutorial uses a free public RPC endpoint. Running your own node is an option for reliability and privacy once you are relying on these checks regularly, but it is not required to get started.
How does a Merkle-tree proof protect my privacy as a user?
A Merkle proof lets you confirm your own balance was included in a published liability snapshot without exposing anyone else’s balance. You only ever see the hashes needed to reconstruct the path from your leaf to the published root, not the underlying data for other accounts.
What should I do if I can’t find a recent attestation report for a stablecoin I hold?
Check the issuer’s current transparency page directly rather than an old bookmark, since reports are frequently moved or archived under new URLs. If there genuinely is no recent report, treat that gap itself as a data point, and weigh it against the GENIUS Act’s monthly-examination expectation for permitted issuers operating in the United States.
