Upstream registry:
config/exchanges/registry.yaml(id: binance) — canonical PoR page + official GitHub repos.
Quick reference for zkPoR verification services: data sources, APIs, and fields. All endpoints are public, unauthenticated, and require no cookies—the same endpoints used by thehttps://www.binance.com/en/proof-of-reserveslanding page itself.
Last verified: 2026-05-05 (current auditId = PR01APR26)
Layer 1 data collection for a PoR verification service requires only 3 HTTPS endpoints + 1 static object store:
| # | Type | Path | Purpose |
|---|---|---|---|
| 1 | GET |
/bapi/apex/v1/public/apex/market/query/auditProofSnapshotCondition |
List all historical snapshots (including BTC block height anchors) |
| 2 | POST |
/bapi/apex/v1/public/apex/market/query/userReserveAuditProofSnapshot |
Fetch the latest snapshot: Merkle Root + full asset/liability breakdown by coin |
| 3 | GET |
/bapi/apex/v1/public/apex/market/por/getDownloadUrl?auditId=... |
Obtain the wallet-address ZIP download URL for any historical snapshot |
| 4 | static | https://public.bnbstatic.com/static/proof-of-reserve/wallet_address_YYYYMMDD.zip |
Reserve-side plaintext inventory (HotCold + Deposit CSVs; see §6) |
The key to bypassing the Cloudflare landing page (HTTP 202): do not scrape HTML—call the BAPI endpoints above directly.
Base URL: https://www.binance.com
GET /bapi/apex/v1/public/apex/market/query/auditProofSnapshotCondition
No parameters. Response:
{
"code": "000000",
"success": true,
"data": [
"01/04/26 00:00:00 UTC | BTC Block Height 943129",
"01/03/26 00:00:00 UTC | BTC Block Height 938780",
"01/02/26 00:00:00 UTC | BTC Block Height 934541",
"..."
]
}
Field semantics:
<MM/DD/YY HH:mm:ss UTC> | BTC Block Height <int>POST /bapi/apex/v1/public/apex/market/query/userReserveAuditProofSnapshot
Content-Type: application/json
Body: {"time":"","pageIndex":0,"pageSize":0}
⚠️ Important finding: The
timefield is currently ignored by the server—any value returns the latest snapshot; a non-empty value that does not match returns an empty object with allnullfields. Historical snapshot Merkle Roots cannot be retrieved via this endpoint (you must consult the announcement for that period or a local cache).
Response (excerpt):
{
"code": "000000",
"success": true,
"data": {
"snapshotTime": "01/04/26 00:00:00 UTC",
"merkleRootHash": "1906250e2d94afd82c37d219ef823ff8852cf2321f1be438af07cd18b4f63f48",
"auditor": "zk-SNARKs",
"auditorLink": "https://www.binance.com/en/support/faq/815b25f0cb054bdd9d35eccc408fe981",
"auditId": "PR01APR26",
"auditDate": "01/04/26",
"snapshotDataList": [
{
"coin": "BTC",
"ratio": "1.0002990000000000",
"customerLiability": 618951.27130074,
"binanceLiability": 619136.126,
"exchangeBalance": 610597.564,
"thirdPartyCustody": 8538.562,
"marginInsurance": null,
"futureInsurance": null,
"customerLiabilityUsdt": 42259690305.68295822
}
]
}
}
Field semantics:
| Field | Meaning |
|---|---|
snapshotTime |
Snapshot timestamp (UTC) |
merkleRootHash |
Poseidon hash root of the current account tree—the core anchor for zkPoR verification |
auditId |
Format PR01APR26; used as input to getDownloadUrl |
auditor |
zk-SNARKs (self-verified) or Mazars / third-party firm name |
auditorLink |
Link to third-party report or verification documentation |
coin.ratio |
binanceLiability / customerLiability; ≥ 1 indicates full reserves |
customerLiability |
Net user liabilities (after margin/contract collateral offsets) |
binanceLiability |
On-chain wallet balances + third-party custody balances |
exchangeBalance |
Binance-owned hot/cold wallet balances |
thirdPartyCustody |
Third-party custody balances |
customerLiabilityUsdt |
USDT-equivalent valuation (display/sorting only; not used in proofs) |
For the current period (PR01APR26), snapshotDataList contains 14 coins in practice: BTC / ETH / USDT / BNB / SOL / USDC / USD1 / XRP / DOGE / PAXG / LINK / U / SUI / LTC.
GET /bapi/apex/v1/public/apex/market/por/getDownloadUrl?auditId=PR01APR26
Response:
{
"code": "000000",
"success": true,
"data": "https://public.bnbstatic.com/static/proof-of-reserve/wallet_address_20260401.zip"
}
auditId naming convention: PR<DD><MMM><YY>, e.g.:
| Snapshot Time | auditId | Wallet ZIP filename |
|---|---|---|
| 01/04/26 | PR01APR26 |
wallet_address_20260401.zip |
| 01/03/26 | PR01MAR26 |
wallet_address_20260301.zip |
| 01/01/26 | PR01JAN26 |
wallet_address_20260101.zip |
| 01/12/25 | PR01DEC25 |
wallet_address_20251201.zip |
Direct URL construction also works (bypassing this API):
https://public.bnbstatic.com/static/proof-of-reserve/wallet_address_<YYYYMMDD>.zip
Verified for 2025-12 through 2026-04: all return HTTP 200, served directly via CloudFront + S3; each file is ~94 MB (application/zip).
| Information | Source | Use |
|---|---|---|
| All historical snapshot times + BTC block heights | 1.1 | Time anchor / snapshot selection |
| Current Merkle Root Hash | 1.2 | Global zk proof anchor |
| Current auditor + auditorLink | 1.2 | Third-party attestation traceability |
| Current ~14 major coins: customer liabilities / on-chain balances / reserve ratio | 1.2 | Layer 1 solvency display |
| Wallet-address inventory ZIP for any historical snapshot (~94 MB per period) | 1.3 / direct link | Self-verify on-chain balances, address clustering |
| Information | Actual source |
|---|---|
| Historical Merkle Root | Binance announcement pages + local archives / third-party mirrors (e.g. DeFiLlama PoR history) |
Per-period zk-SNARK proof.csv / cex_assets_info.json / verifying key |
Binance announcements + logged-in user download; verifier tool only on GitHub — see registry binance |
| A user’s Merkle inclusion proof JSON | User must log in → Wallet → Verification → “Download Merkle Tree” |
| Asset details for non-Top-14 coins | Same as above (full list only in cex_assets_info.json inside the zip) |
Rule of thumb: global/aggregate data via BAPI; user-level/raw proof artifacts via static object storage or the user dashboard.
const r = await fetch(
"https://www.binance.com/bapi/apex/v1/public/apex/market/query/userReserveAuditProofSnapshot",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ time: "", pageIndex: 0, pageSize: 0 }),
},
).then((x) => x.json());
console.log(r.data.merkleRootHash); // -> 1906250e2d94...
console.log(r.data.snapshotTime); // -> 01/04/26 00:00:00 UTC
console.log(r.data.auditId); // -> PR01APR26
const list = await fetch(
"https://www.binance.com/bapi/apex/v1/public/apex/market/query/auditProofSnapshotCondition",
).then((x) => x.json());
const parsed = list.data.map((s: string) => {
const [time, btc] = s.split(" | ");
return {
time,
btcBlockHeight: Number(btc.replace(/\D/g, "")),
};
});
auditId and ZIP URL from snapshotTimeconst MONTHS = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"];
function toAuditId(snapshotTime: string) {
// "01/04/26 00:00:00 UTC" -> "PR01APR26"
const [mdy] = snapshotTime.split(" ");
const [mm, dd, yy] = mdy.split("/");
return `PR${dd}${MONTHS[Number(mm) - 1]}${yy}`;
}
function toWalletZipUrl(snapshotTime: string) {
const [mm, dd, yy] = snapshotTime.split(" ")[0].split("/");
return `https://public.bnbstatic.com/static/proof-of-reserve/wallet_address_20${yy}${mm}${dd}.zip`;
}
# Current Root
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"time":"","pageIndex":0,"pageSize":0}' \
https://www.binance.com/bapi/apex/v1/public/apex/market/query/userReserveAuditProofSnapshot \
| jq '.data | {snapshotTime, merkleRootHash, auditId}'
# Current wallet ZIP URL
curl -s 'https://www.binance.com/bapi/apex/v1/public/apex/market/por/getDownloadUrl?auditId=PR01APR26' \
| jq -r '.data'
data[0] shows a new entry. Binance publishes a new snapshot at the start of each month—typical cadence is once per month.merkleRootHash ↔ account_tree_root in proof.csv from the GitHub ReleasesnapshotTime ↔ BTC Block Height ↔ block timestamp on mempool.spacebinanceLiability ↔ aggregated on-chain balances of wallet addresses in the zipverify.sh that outputs PASS / FAIL.https://www.binance.com/en/proof-of-reserves returns HTTP 202 + empty body on direct curl (CF challenge), but BAPI paths do not go through CF anti-bot.time field in userReserveAuditProofSnapshot is a legacy parameter; the server currently only supports an empty string = latest. Passing any specific timestamp returns data with all null. Do not mistake this for a broken endpoint.getDownloadUrl must use a query string (?auditId=...); a JSON body POST returns code: 000002 illegal parameter.The wallet_address_<YYYYMMDD>.zip obtained via §1.3 is not a zk proof file—it is Binance’s plaintext inventory of all reserve addresses for that period. This is the only public, independently auditable hard evidence on the PoR “reserve side.”
Using PR01APR26 as an example (downloaded and verified 2026-05-05):
| File | Size | Row count (order of magnitude) | Contents |
|---|---|---|---|
PR01APR26_HotCold.csv |
110 KB | ~10³ | Binance-owned hot/cold wallets |
PR01APR26_Deposit.csv |
275 MB | ~10⁷ | All user deposit addresses |
The ZIP itself is ~94 MB (gzip compressed); ~275 MB uncompressed. HotCold covers the high-concentration dimension; Deposit covers the long tail.
coin,network,address,balance,Height,Third party custodian name
USDC,ALGO,QYXDGS2XJJT7QNR6EJ2YHNZFONU6ROFM6BKTBNVT63ZXQ5OC6IYSPNDJ4U,27405800.080000000000000000,59804916,""
ETH,ETH,0x57a0dfd29d8aa63a34acddb8dce2910b7e98a646,0.031817860000000000,24781026,""
| Column | Meaning | Notes |
|---|---|---|
coin |
Asset symbol (BTC/ETH/USDT/…) | Matches §1.2 snapshotDataList[].coin |
network |
Chain for this address (BTC/ETH/BSC/TRX/SOL/APT/ALGO/…) | Same coin may span multiple chains (especially USDT) |
address |
Plaintext on-chain address | Can be pasted directly into a block explorer |
balance |
Balance at block Height |
Binance-reported value—requires independent verification |
Height |
Block height at which balance was taken | Per-network height; enables archive-node lookup |
Third party custodian name |
Third-party custodian name (if address is custodied externally) | Reconcile against §1.2 thirdPartyCustody |
Σ_{rows where coin=X} balance = binanceLiability[X] (from §1.2)
= exchangeBalance[X] + thirdPartyCustody[X]
Where:
Third party custodian name == "" → count toward exchangeBalanceThird party custodian name != "" → count toward thirdPartyCustodyThis yields a fully public, one-click reproducible internal consistency check.
This is the strongest PoR verification achievable through public channels, with no dependency on any zk toolchain:
for each row (coin, network, address, balance_claim, height) in CSV:
real_balance := chain_archive_node(network).getBalance(address, height)
if abs(real_balance - balance_claim) > ε:
flag(row) # Binance-reported value does not match on-chain
aggregated_real[coin] := Σ real_balance per coin
aggregated_real[coin] >= customerLiability[coin] ? # PASS / FAIL
Implementation notes:
eth_getBalance with blockNumber). For EVM chains, self-host Erigon/Reth; for BTC use electrs + getblock; for Solana SPL use a slot-indexed API (SOLANA_INDEX_API_KEY → Solana Index GET /api/v1/solana/token-balance/{address}/{mint}/{slot}); standard getTokenAccountsByOwner has no historical state.balance head is sufficient).# Download + verify + extract
URL=$(curl -s "https://www.binance.com/bapi/apex/v1/public/apex/market/por/getDownloadUrl?auditId=PR01APR26" | jq -r '.data')
curl -L "$URL" -o wallet.zip
shasum -a 256 wallet.zip # Archive: publish SHA256 + download timestamp signature
unzip wallet.zip -d ./wallet/
# Internal consistency: HotCold + Deposit sums vs BAPI binanceLiability
python3 - <<'PY'
import csv, json, urllib.request
from collections import defaultdict
from decimal import Decimal
agg_self, agg_3p = defaultdict(Decimal), defaultdict(Decimal)
for fn in ['wallet/PR01APR26_HotCold.csv', 'wallet/PR01APR26_Deposit.csv']:
with open(fn) as f:
for row in csv.DictReader(f):
bal = Decimal(row['balance'])
(agg_3p if row['Third party custodian name'] else agg_self)[row['coin']] += bal
api = json.loads(urllib.request.urlopen(urllib.request.Request(
"https://www.binance.com/bapi/apex/v1/public/apex/market/query/userReserveAuditProofSnapshot",
data=b'{"time":"","pageIndex":0,"pageSize":0}',
headers={"Content-Type":"application/json"}
)).read())['data']['snapshotDataList']
for c in api:
coin = c['coin']
print(f"{coin:8s} csv_self={agg_self[coin]:>20} api_self={c['exchangeBalance']:>20} "
f"csv_3p={agg_3p[coin]:>20} api_3p={c['thirdPartyCustody']:>20}")
PY
After running, CSV aggregates should align one-to-one with BAPI-reported values across all 14 major coins (decimal error < 1e-8). Any discrepancy means Binance’s two data sources (frontend aggregation vs. wallet export) are already inconsistent—a very strong red flag.
| Layer | Data source | What it proves | Depends on Binance private data? |
|---|---|---|---|
| L1 Internal consistency | §1.2 BAPI + §1.3 CSV | Whether Binance’s two self-reported datasets are mutually consistent | No |
| L2 Independent on-chain audit | §1.3 CSV + public chain archives | Reserve addresses hold real balances at snapshot height ≥ user liabilities | No |
| L2b Deposit user spot check | deposit-sample-manifest + block explorer | Independently reproduce any row from ardmere’s sampling conclusions | No |
| L3 User-level zk inclusion | User-downloaded zip after login + GitHub verifier | Individual user balance is included in the tree + full tree is non-negative | Yes (private zip) |
Public services can deliver L1+L2; L3 must be implemented as user-local in-browser WASM verification.
None of the APIs documented here appear in Binance’s public documentation—they were discovered by reverse-engineering the landing page JS chunks. Reproduction steps:
curl https://www.binance.info/en/proof-of-reserves -o por.html (.info domain uses SSR, no CF blocking)webpack-runtime.*.js to obtain the chunk id → hash mappingmain.*.js, the proof-of-reserves route maps to chunk id 9a3ahttps://bin.bnbstatic.com/static/chunks/page-9a3a.<hash>.js/bapi/ inside that chunk to find the three endpoints in §1 and their call patterns