On August 13, 2026, Tether announced that KPMG U.S. had finished the company’s first full independent audit, an unqualified opinion covering fiscal year 2025. Reserves beat liabilities by $6.814 billion as of December 31, 2025. Auditors reportedly counted every physical gold bar Tether holds. It’s the kind of headline that makes a $183 billion stablecoin feel a lot safer overnight.
But a headline number is not the same as knowing how to check it yourself. Audits happen once a year. Attestations are a snapshot of a single day. Prices move every block. If you hold stablecoins, build on top of them, or run a treasury that depends on one, you need a repeatable way to verify reserves that doesn’t rely on trusting a press release. This tutorial walks through exactly that: reading the primary documents, pulling on-chain proof-of-reserve data with Python, verifying Merkle-tree proofs for exchanges, building a Dune Analytics dashboard, and wiring up automated alerts so you find out about a reserve shortfall before Twitter does.
By the end you’ll have a working command-line tool that checks stablecoin proof-of-reserves data on a schedule and pings you if the numbers look wrong. Total build time is roughly 45 minutes if you already have Python installed.
Why You Shouldn’t Just Trust the Audit Headline
Tether’s KPMG audit is genuinely a big deal. It’s being described as one of the largest inaugural financial audits ever performed for a crypto company, and it involved substantive testing rather than a light-touch review. But an annual audit only tells you the balance sheet looked healthy on one specific date, December 31, 2025 in this case. Between audits, issuers publish quarterly or monthly attestations, which are narrower in scope and only confirm a snapshot balance, not a full set of financial statements.
That gap matters because reserve buffers move. Composition shifts between Treasury bills, reverse repo, gold, and other assets from one reporting period to the next. A trader deciding whether to hold a large USDT or USDC position overnight doesn’t have the luxury of waiting for next year’s audit. Neither does a DeFi protocol accepting a stablecoin as collateral. That’s the entire reason proof-of-reserves tooling exists: to give you a way to check the math yourself, continuously, instead of taking one annual snapshot on faith.
This tutorial treats stablecoin proof-of-reserves verification as an engineering problem. You’ll read primary sources, pull data programmatically, and build monitoring that runs without you refreshing a transparency page every morning.
What Actually Changed With Tether’s 2026 Audit
Before August 2026, Tether’s reserve disclosures came entirely from quarterly attestations performed by BDO, confirming a snapshot balance rather than a full set of audited financial statements. The KPMG engagement changed that by examining Tether International’s complete financial statements for the year ended December 31, 2025, under U.S. GAAP, and issuing a formal unqualified opinion. As part of the work, auditors physically counted and inspected every individual gold bar Tether holds, verifying identifying details rather than relying on custodian paperwork alone. That’s a materially deeper level of scrutiny than an attestation provides, and it’s why the crypto press described it as one of the largest inaugural financial audits ever performed for a company in this industry.
What the audit does not do is replace the need for ongoing checks. It covers one fiscal year that ended nearly eight months before you’re reading this. Tether’s quarterly BDO attestations continue in between annual audits, and that’s exactly the layer this tutorial’s automation is built to supplement with live, on-chain data.
Audit vs Attestation vs Proof-of-Reserves: Know the Difference
These three terms get used interchangeably in headlines, and that’s part of the problem. Each one proves something different, performed by different parties, on a different schedule. Table 1 breaks it down.
| Verification Method | Who Performs It | Typical Frequency | What It Actually Proves | Real Example |
|---|---|---|---|---|
| Full financial audit | Big Four accounting firm (e.g. KPMG) | Annual | Complete financial statements under GAAP, formal opinion on fair presentation | Tether FY2025 audit, unqualified opinion, Aug 13, 2026 |
| Attestation / examination | Assurance firm under AICPA AT-C 205 or ISAE 3000 (Revised) | Monthly or quarterly | Point-in-time confirmation that reserves meet or exceed liabilities on one stated date | Circle’s monthly USDC examinations; Tether’s quarterly BDO reports |
| On-chain Proof-of-Reserve oracle | Oracle network reading custodian/issuer data feeds | Continuous, updated every block or on a heartbeat | A live reserve balance a smart contract can read and act on automatically | Chainlink Proof of Reserve |
| Merkle-tree proof-of-reserves | Exchange plus an independent auditor | Periodic snapshot, usually quarterly | That your specific account balance is included in the published total liabilities set | Exchange-published PoR trees for user balances |
Notice that none of these four methods, on its own, gives you continuous, trustless certainty. An audit is thorough but stale within weeks. An attestation is frequent but narrow. An on-chain oracle is live but only as good as the data source feeding it. A Merkle proof tells you that you’re included in a set, not that the set is fully backed. The rest of this tutorial builds a workflow that layers all four together, because that’s what actually closes the gap.
Prerequisites: Tools, Accounts, and Versions
Before you start, get these installed and set up. None of it costs money for the scale of checking used in this tutorial.
- Python 3.11 or newer (check with
python3 --version) web3.pyversion 6.x for reading on-chain datarequestsandpandasfor HTTP calls and data wrangling- A free RPC endpoint from a provider like Alchemy or Infura (or a public endpoint for light use)
- A free Dune Analytics account for building a continuous tracking dashboard
- A text editor and terminal access; this tutorial assumes macOS or Linux shell syntax
- Optional: a Slack or Discord webhook URL if you want automated alerts in Step 9
You do not need a paid API tier for anything in this tutorial. Free-tier RPC limits (typically a few hundred requests per second across providers) are far more than enough for periodic reserve checks.
One more thing worth doing before you write any code: bookmark the specific attestation and audit pages for every stablecoin you plan to monitor, not just the issuer’s homepage. Transparency pages get restructured periodically, and having the direct PDF or report URL saved saves you from hunting through a redesigned site the next time you need to cross-check a number.
Step 1: Locate and Read the Primary Source Report
Skip the press release and go straight to the issuer’s own transparency page. For Tether, that’s tether.to/en/transparency. For Circle’s USDC, it’s circle.com/transparency. Both publish the actual attestation or audit PDF, not just a summary.
When you open the document, check four things before you read a single number: who performed it (the named firm, not “an independent auditor”), what standard they used (GAAP audit, AICPA AT-C 205 examination, or ISAE 3000 Revised attestation), what date the figures are “as of,” and whether the opinion is qualified or unqualified. An unqualified opinion means the firm found no material issues. A qualified opinion means they found something worth flagging, and that’s worth reading closely rather than skimming past.
For the KPMG audit of Tether, the standard was U.S. GAAP, the opinion was unqualified, and the “as of” date was December 31, 2025. For Circle’s monthly USDC reports, the standard is AICPA AT-C Section 205, reserve holdings are disclosed weekly, and a Big Four accounting firm signs off monthly under that examination standard, according to Circle’s own transparency page.
Step 2: Extract the Balance Sheet Into a Spreadsheet
Reserve reports break assets into categories: cash and cash equivalents, U.S. Treasury bills, overnight and term reverse repurchase agreements, corporate bonds, secured loans, precious metals, and sometimes a Bitcoin position. Pull each line item into a spreadsheet with a column for the reporting date, so you can track how composition shifts between reports.
Using Tether’s audited December 31, 2025 figures as a worked example: reporting around the KPMG audit put roughly $141 billion of the reserve in U.S. Treasury securities, with the remainder split across gold holdings, a Bitcoin position, and other instruments, against total token liabilities that leave a surplus of $6.814 billion. The reserve ratio is simple to compute once you have both sides:
reserve_ratio = total_reserve_assets / total_tokens_outstanding
# Example using the audited Dec 31, 2025 figures
total_reserve_assets = 183_000_000_000 + 6_814_000_000 # liabilities + surplus
total_tokens_outstanding = 183_000_000_000
reserve_ratio = total_reserve_assets / total_tokens_outstanding
print(f"Reserve ratio: {reserve_ratio:.4f}") # anything above 1.0000 is a surplus
Anything meaningfully above 1.0 is a healthy buffer. A ratio that drifts toward exactly 1.0 over successive reports is a signal worth watching, even if no single report shows a deficit.
Reading Reserve Composition Line by Line
Don’t stop at the total. The composition of a reserve matters almost as much as its size, because different asset types carry different liquidity risk. Short-dated U.S. Treasury bills convert to cash quickly and predictably. Corporate bonds and secured loans take longer and carry credit risk. Gold and Bitcoin positions add price volatility that a pure cash reserve wouldn’t have. When you log a reporting period into your spreadsheet, break out at least these five buckets separately: cash and equivalents, Treasury bills, reverse repo, other investments (bonds, secured loans, precious metals, digital assets), and any explicitly disclosed custodian names. Tracking the percentage each bucket represents over several reporting periods will show you whether an issuer is drifting toward riskier, less liquid holdings well before that shows up as a headline number.
Step 3: Install Your Verification Toolkit
Now set up the Python environment you’ll use for the rest of the tutorial. Create a virtual environment first so these packages don’t collide with anything else on your machine.
python3 -m venv por-checker
source por-checker/bin/activate
pip install web3==6.20.3 requests==2.32.3 pandas==2.2.3
mkdir por-checker-project && cd por-checker-project
Confirm the install worked before moving on:
python3 -c "import web3, requests, pandas; print(web3.__version__)"
Step 4: Read a Chainlink Proof-of-Reserve Feed On-Chain
Chainlink’s Proof of Reserve product connects off-chain reserve data directly to smart contract logic, so a protocol can automatically cap redemptions, pause minting, or trigger a circuit breaker if reserves fall below a threshold. PoR feeds use the same AggregatorV3Interface pattern as Chainlink’s price feeds, which means if you’ve ever read a price feed you already know this API, per Chainlink’s own documentation.
from web3 import Web3
RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"
POR_FEED_ADDRESS = "0xYourTargetPoRFeedAddressHere"
AGGREGATOR_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",
},
]
w3 = Web3(Web3.HTTPProvider(RPC_URL))
feed = w3.eth.contract(address=Web3.to_checksum_address(POR_FEED_ADDRESS), abi=AGGREGATOR_ABI)
decimals = feed.functions.decimals().call()
round_id, answer, started_at, updated_at, answered_in_round = feed.functions.latestRoundData().call()
reserve_value = answer / (10 ** decimals)
print(f"Reported reserves: {reserve_value:,.2f}")
print(f"Last updated (unix): {updated_at}")
Replace POR_FEED_ADDRESS with the actual contract address for the feed you’re checking; Chainlink publishes the current addresses in its documentation. The updated_at timestamp is worth checking on every call. A PoR feed that hasn’t updated in an unusually long window is itself a signal, even before you look at the reported value.
Step 5: Pull Circulating Supply and Compare Reserve Ratios
A reserve number by itself doesn’t tell you much until you compare it against how many tokens are actually in circulation. ERC-20 tokens expose a standard totalSupply() function, so you can pull both sides of the equation in the same script.
ERC20_ABI = [
{
"constant": True,
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function",
},
{
"constant": True,
"inputs": [],
"name": "decimals",
"outputs": [{"name": "", "type": "uint8"}],
"type": "function",
},
]
TOKEN_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec" # USDT contract
token = w3.eth.contract(address=Web3.to_checksum_address(TOKEN_ADDRESS), abi=ERC20_ABI)
token_decimals = token.functions.decimals().call()
total_supply = token.functions.totalSupply().call() / (10 ** token_decimals)
live_ratio = reserve_value / total_supply
print(f"Circulating supply: {total_supply:,.2f}")
print(f"Live PoR reserve ratio: {live_ratio:.4f}")
if live_ratio < 1.0:
print("WARNING: reported reserves are below circulating supply")
This is the core check you'll automate later in Step 9. Run it once manually first so you understand what a healthy ratio looks like for the token you're monitoring before you let a script watch it unattended.
Step 6: Verify a Merkle-Tree Proof-of-Reserves for an Exchange
Exchanges publish a different kind of proof: a Merkle tree where every user's balance is a leaf, and the root hash is published publicly. If you can reconstruct the path from your leaf to the published root, you've proven your balance is included in what the exchange claims it owes, without seeing anyone else's balance.
import hashlib
def hash_leaf(user_id: str, balance: int) -> bytes:
data = f"{user_id}:{balance}".encode()
return hashlib.sha256(data).digest()
def hash_pair(left: bytes, right: bytes) -> bytes:
# Sort so the proof is order-independent, matching common PoR implementations
ordered = sorted([left, right])
return hashlib.sha256(ordered[0] + ordered[1]).digest()
def verify_merkle_proof(leaf: bytes, proof: list, published_root: bytes) -> bool:
computed = leaf
for sibling in proof:
computed = hash_pair(computed, sibling)
return computed == published_root
# Example usage: exchange gives you your leaf hash, sibling proof, and the published root
my_leaf = hash_leaf("user_12345", 5000_00) # balance in cents
sibling_proof = [bytes.fromhex("a1b2c3")] # supplied by the exchange's PoR tool
published_root = bytes.fromhex("f9e8d7") # published on the exchange's transparency page
is_included = verify_merkle_proof(my_leaf, sibling_proof, published_root)
print(f"Balance included in published reserves: {is_included}")
Fill in the real leaf hash, sibling proof, and published root from the exchange's own PoR download tool; most major exchanges now provide one directly in the account interface. If is_included comes back False, that's not proof of fraud on its own, but it means the exchange's proof data doesn't match what it's telling you, and it's worth a support ticket before you conclude anything.
Step 7: Build a Dune Analytics Query for Continuous Tracking
One-off checks are useful, but reserves change composition over time, and a single script run doesn't show you the trend. Dune Analytics lets you write SQL against indexed on-chain data and turn it into a dashboard you can revisit or share. Create a free account, open a new query, and start with something like this against an ERC-20 transfer table.
-- Track daily net supply change for a stablecoin contract
SELECT
date_trunc('day', evt_block_time) AS day,
SUM(CASE WHEN "from" = 0x0000000000000000000000000000000000000000
THEN value ELSE 0 END) / 1e6 AS minted,
SUM(CASE WHEN "to" = 0x0000000000000000000000000000000000000000
THEN value ELSE 0 END) / 1e6 AS burned
FROM erc20_ethereum.evt_Transfer
WHERE contract_address = 0xdAC17F958D2ee523a2206206994597C13D831ec
AND evt_block_time >= NOW() - INTERVAL '90' DAY
GROUP BY 1
ORDER BY 1 DESC
Save this as a query, then pin it to a dashboard alongside a chart. Watching mint and burn volume over 90 days gives you a feel for whether supply growth is outpacing what the issuer's most recent attestation supports. A sudden multi-day minting spike with no corresponding attestation update is exactly the kind of gap this tutorial is built to catch.
Step 8: Cross-Reference Treasury and Custodian Filings
Stablecoin issuers aren't registered investment companies, so they don't file the same disclosures a mutual fund would. But when an issuer states it holds a specific dollar amount in U.S. Treasury bills, you can still sanity-check the claim against publicly available Treasury market data and named custodian relationships disclosed in the issuer's own reports.
Read the custodian and counterparty names listed in the attestation or audit report itself, not a summary article. Then check whether those names are consistent from one report to the next. A custodian relationship that changes without explanation between reporting periods is worth a closer look, even if the headline reserve number stays flat. This step is manual by nature, there's no clean API for cross-checking private custodial relationships, but it takes fifteen minutes and it's the step most people skip.
Keep a running note of which custodians and counterparties appear in each report you read, alongside the date. Over two or three reporting cycles, this manual log becomes a useful cross-check on its own: consistent custodian names across multiple periods is a mild positive signal, while frequent, unexplained changes is worth raising as a question to the issuer directly, something you can do through most issuers' public investor relations or support channels.
Step 9: Automate Reserve-Ratio Alerts and Log Findings
Now combine Steps 4 and 5 into a script that runs on a schedule and posts to a webhook if the ratio drops below a threshold you set. This is the piece that turns a one-time check into ongoing monitoring.
import requests
import json
from datetime import datetime, timezone
WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
ALERT_THRESHOLD = 1.0
def check_and_alert(live_ratio: float, token_symbol: str):
timestamp = datetime.now(timezone.utc).isoformat()
log_entry = {"timestamp": timestamp, "symbol": token_symbol, "ratio": round(live_ratio, 4)}
with open("por_log.jsonl", "a") as f:
f.write(json.dumps(log_entry) + "\n")
if live_ratio < ALERT_THRESHOLD:
message = {
"text": f":rotating_light: {token_symbol} reserve ratio dropped to {live_ratio:.4f} "
f"at {timestamp}. Reported reserves are below circulating supply."
}
requests.post(WEBHOOK_URL, json=message, timeout=10)
print("Alert sent.")
else:
print(f"{token_symbol} ratio healthy at {live_ratio:.4f}, logged, no alert sent.")
check_and_alert(live_ratio, "USDT")
Set this up as a cron job or scheduled task to run every few hours. The por_log.jsonl file becomes your own historical record, independent of anyone else's dashboard, which matters if you ever need to show what you knew and when.
Step 10: Set a Recheck Cadence and Build Your Trust Score
The last step is process, not code. Decide how often you'll run each check, and give yourself a simple rubric for how much confidence to place in a given stablecoin's reserves. A reasonable cadence: run the on-chain PoR and supply check (Steps 4-5) daily via the automation from Step 9, review the Dune dashboard (Step 7) weekly, and re-read the primary attestation or audit document (Step 1) every time the issuer publishes a new one.
Score each issuer on five factors: audit recency (within the last 12 months), attestation frequency (monthly beats quarterly), PoR feed freshness (updated_at within the expected heartbeat window), Merkle proof availability for exchange balances, and named regulatory registration status such as MiCA compliance in the EU. No single factor is disqualifying on its own, but an issuer scoring low across three or more is one you should size positions around more conservatively.
Complete Working Project: The Full Verification Script
Here's everything from Steps 4, 5, and 9 combined into one runnable command-line tool. Save it as por_check.py and run it with python3 por_check.py after filling in your own RPC URL, feed address, and webhook.
import json
import requests
from datetime import datetime, timezone
from web3 import Web3
RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"
POR_FEED_ADDRESS = "0xYourTargetPoRFeedAddressHere"
TOKEN_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec"
WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
ALERT_THRESHOLD = 1.0
TOKEN_SYMBOL = "USDT"
AGGREGATOR_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"},
]
ERC20_ABI = [
{"constant": True, "inputs": [], "name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}], "type": "function"},
{"constant": True, "inputs": [], "name": "decimals",
"outputs": [{"name": "", "type": "uint8"}], "type": "function"},
]
def run_check():
w3 = Web3(Web3.HTTPProvider(RPC_URL))
feed = w3.eth.contract(address=Web3.to_checksum_address(POR_FEED_ADDRESS), abi=AGGREGATOR_ABI)
feed_decimals = feed.functions.decimals().call()
_, answer, _, updated_at, _ = feed.functions.latestRoundData().call()
reserve_value = answer / (10 ** feed_decimals)
token = w3.eth.contract(address=Web3.to_checksum_address(TOKEN_ADDRESS), abi=ERC20_ABI)
token_decimals = token.functions.decimals().call()
total_supply = token.functions.totalSupply().call() / (10 ** token_decimals)
live_ratio = reserve_value / total_supply
timestamp = datetime.now(timezone.utc).isoformat()
log_entry = {
"timestamp": timestamp, "symbol": TOKEN_SYMBOL,
"reserve_value": reserve_value, "total_supply": total_supply,
"ratio": round(live_ratio, 4), "feed_updated_at": updated_at,
}
with open("por_log.jsonl", "a") as f:
f.write(json.dumps(log_entry) + "\n")
print(json.dumps(log_entry, indent=2))
if live_ratio < ALERT_THRESHOLD:
message = {"text": f":rotating_light: {TOKEN_SYMBOL} PoR ratio dropped to "
f"{live_ratio:.4f} at {timestamp}."}
requests.post(WEBHOOK_URL, json=message, timeout=10)
print("Alert sent.")
if __name__ == "__main__":
run_check()
Running it produces output like this:
{
"timestamp": "2026-08-23T14:02:11.483921+00:00",
"symbol": "USDT",
"reserve_value": 189734281044.22,
"total_supply": 183021554871.10,
"ratio": 1.0367,
"feed_updated_at": 1787432521
}
A ratio above 1.0 with a recent feed_updated_at timestamp is what you want to see on every run. Wire the script into cron (crontab -e, then 0 */6 * * * /path/to/venv/bin/python3 /path/to/por_check.py) and you have a monitoring pipeline that runs itself.
How Regulation Is Raising the Verification Bar
Reserve verification isn't just a technical exercise anymore, it's increasingly a regulatory requirement. The EU's Markets in Crypto-Assets regulation (MiCA) imposes reserve and disclosure obligations on stablecoin issuers operating in the bloc, and commentary around Tether's KPMG audit has specifically framed a demonstrated reserve surplus and a Big Four audit opinion as relevant to how an issuer positions itself under those rules. The practical effect for you as someone running your own verification pipeline: issuers operating under stricter regulatory regimes tend to publish more granular, more frequent, and more consistently formatted disclosures, because they have a compliance obligation to do so, not just a reputational incentive.
This is worth factoring into the trust-score rubric from Step 10. An issuer with a named regulatory registration and a documented compliance framework has an external party (a regulator) also checking its disclosures, which is a different kind of assurance layer than an audit or attestation performed at the issuer's own request. It doesn't replace your own checks, but it's a legitimate input into how much scrutiny you apply and how often you recheck.
Common Pitfalls When Verifying Reserves Yourself
- Confusing an attestation with an audit. A monthly attestation confirms one day's balance under a narrower standard than a full GAAP audit. Treat them as different confidence levels, not interchangeable proof.
- Trusting a single PoR feed without checking its update timestamp. A stale feed can keep reporting a healthy-looking number long after the underlying reserve data stopped updating. Always check
updated_at, not justanswer. - Assuming a Merkle proof means the exchange is solvent. It only proves your balance is included in the published liability set. It says nothing about whether the exchange's claimed assets actually exist.
- Hardcoding a token decimals value instead of reading it from the contract. Most stablecoins use 6 decimals, but not all, and a hardcoded assumption silently produces wrong ratios for other tokens.
- Relying on a single RPC provider. Free-tier RPC endpoints occasionally return stale or rate-limited responses. A monitoring script that only checks one provider can report a false alarm, or worse, miss a real one.
- Comparing figures from different reporting dates as if they were simultaneous. A June attestation and an August audit describe two different snapshots. Always note the "as of" date before drawing a conclusion from a comparison.
- Treating "Big Four accounting firm" as equivalent across every issuer. The firm's name matters less than the standard applied and the scope of the engagement. A monthly examination and an annual audit performed by the same firm are still two very different levels of assurance.
- Ignoring gas costs when polling on-chain data too frequently. Read calls like
latestRoundData()andtotalSupply()don't cost gas when called off-chain through an RPC provider, but hammering a rate-limited free-tier endpoint every few seconds will get you throttled. Poll on a sensible interval, not continuously.
Troubleshooting Guide
Most issues you'll hit while building this pipeline fall into a handful of categories: connectivity, address mismatches, decimals errors, and scheduling. The table below covers the ones that come up most often, in roughly the order you're likely to encounter them while working through Steps 3 through 9.
| Problem | Likely Cause | Fix |
|---|---|---|
| web3.py throws "Could not connect to any providers" | Invalid or rate-limited RPC URL | Verify your API key and try a different provider or the public RPC endpoint |
| PoR feed call reverts | Wrong contract address for the network you're connected to | Confirm you're on the correct chain (mainnet vs L2) and re-check the feed address in Chainlink's docs |
| Reserve ratio looks impossibly high or low | Decimals mismatch between the feed and the token contract | Print both decimals values separately before dividing; don't assume they match |
totalSupply() call fails on an ERC-20 contract | Contract uses a non-standard or proxy implementation | Check the contract on a block explorer for a proxy address and use the implementation ABI instead |
| Merkle proof verification returns False for a valid balance | Leaf hashing order or format doesn't match the exchange's implementation | Re-read the exchange's PoR documentation for the exact hashing scheme; sorting order and encoding vary between platforms |
| Dune query times out | Querying too wide a date range on a high-volume table | Narrow the evt_block_time window and add an index-friendly filter on contract_address |
| Webhook alert never fires even when ratio drops | Threshold comparison logic runs before the value is fully assigned, or timeout too short | Add print statements to confirm live_ratio before the conditional, and raise the requests.post timeout |
| Cron job doesn't run | Cron uses a different Python environment than your shell | Use the full path to your virtual environment's Python binary in the crontab entry |
Advanced Tips for Ongoing Monitoring
Once the basic pipeline is running, a few upgrades make it meaningfully more useful. First, add a second RPC provider as a fallback and only alert when both agree on a low ratio, which cuts down false positives from a single flaky endpoint. Second, cache the previous run's result locally and diff against it, so your alert message shows the trend ("ratio fell from 1.041 to 1.019 over 24 hours") instead of just a static number. Third, extend the script to loop over multiple stablecoins in one pass and log them to the same file, so your Dune dashboard and your local por_log.jsonl cover your whole portfolio, not just one token.
Finally, if you're building this for a team rather than personal use, split the alerting threshold into two tiers: a quiet log-only tier for minor fluctuations, and a paging tier for anything that crosses a hard floor you define ahead of time. That keeps the noise down while still catching the events that actually matter.
It's also worth version-controlling your por_check.py script and your Dune queries once you're relying on them regularly. Reserve feed contract addresses occasionally migrate when an oracle network upgrades its infrastructure, and a script sitting in a private git repo with commit history is much easier to fix at 2 a.m. than one you vaguely remember editing on a laptop six months ago. Tag each config change with the date and the reason, the same discipline you'd apply to any other production monitoring code.
Stablecoin Verification Practices Compared
Table 2 summarizes how the two largest stablecoin issuers currently handle verification, based on their own published transparency pages as of August 2026.
| Issuer | Latest Full Audit | Attestation Frequency | Assurance Standard | Latest Reported Buffer |
|---|---|---|---|---|
| Tether (USDT) | KPMG U.S., unqualified opinion, Aug 13, 2026 (FY2025) | Quarterly (BDO) | US GAAP (audit) / ISAE 3000 Revised (attestation) | $6.814B surplus as of Dec 31, 2025 |
| USDC (Circle) | Not a full annual GAAP audit; monthly examinations instead | Monthly, with weekly reserve holdings disclosure | AICPA AT-C Section 205 examination | Reserves reported to meet or exceed circulating supply each month, per Circle's transparency page |
The practical takeaway: Tether now has the deeper one-time audit, while Circle has the higher-frequency disclosure cadence. Neither replaces the other, which is exactly why the multi-layer verification workflow in this tutorial checks both continuous on-chain signals and periodic primary-source documents rather than picking just one.
Frequently Asked Questions
Is a stablecoin attestation the same as an audit?
No. An attestation, typically performed under AICPA AT-C 205 or ISAE 3000 (Revised), confirms a single balance on one date. A full audit examines complete financial statements under GAAP and results in a formal opinion. Tether's August 2026 KPMG report was its first full audit; its earlier reports were quarterly attestations from BDO.
Can I verify stablecoin reserves without running any code?
Partially. You can read the primary attestation or audit PDF (Step 1) and check an exchange's published Merkle root (Step 6) manually. But continuous, automated checking of on-chain PoR feeds and supply ratios (Steps 4, 5, and 9) requires the scripting shown in this tutorial.
What does Chainlink Proof of Reserve actually monitor?
It connects off-chain reserve data to on-chain smart contract logic, so protocols can automatically cap redemptions, pause minting, or trigger a circuit breaker if reported reserves drop below a set threshold, functioning through the same AggregatorV3Interface pattern used by Chainlink's price feeds.
How often should I re-run the reserve ratio check?
Daily is reasonable for personal monitoring using the automated script from Step 9. If you're managing a treasury or a protocol with real exposure, consider running it every few hours and layering in a second RPC provider as described in the advanced tips.
Does a reserve ratio above 1.0 guarantee a stablecoin is safe?
No. It confirms reported reserves cover circulating supply at that moment, based on whatever data source feeds the check. It doesn't independently verify that the underlying assets exist or are unencumbered, that's what the audit and attestation layers are for.
Why did Tether's reserve buffer figure change between reports?
Reserve composition shifts as issuers rebalance between Treasury bills, reverse repo, and other instruments, and each attestation captures a different snapshot date. This is exactly why relying on one headline figure is riskier than tracking the trend across multiple reports.
Is Merkle-tree proof-of-reserves the same thing as an on-chain PoR oracle?
No. A Merkle proof shows your balance is included in an exchange's published liability set at a point in time. An on-chain PoR oracle is a live, continuously updated feed a smart contract can query. They answer different questions and are both covered separately in this tutorial.
What happens if I don't have an RPC provider API key?
Public RPC endpoints exist for most major chains and work fine for the low request volume in this tutorial, though they're more prone to rate limiting under heavy use. A free-tier key from a provider like Alchemy or Infura is the more reliable option and takes about five minutes to set up.
Do I need to run this on Ethereum mainnet specifically?
No. The same pattern works on any EVM-compatible chain where a PoR feed and the token contract are both deployed, you just need the correct RPC endpoint and contract addresses for that network. Layer 2 networks and other EVM chains expose the same AggregatorV3Interface and ERC-20 standards, so the code from Steps 4 and 5 carries over with only the addresses changed.



