[requires: http]
GET /api/v1/verify/proof-escrow/{id} is a convenience read. It is not the
trust story. After a proof-gated query you already hold everything needed to
prove, against Base Sepolia and the published spec, that (1) the escrow
released and (2) the bytes you received sit under the root the contract
committed at create time.
Anyone can run this. You do not need to trust the API's RELEASED flag.
Sections 1 (escrow decode) and 2 (bytes vs root) run on public inputs only —
no credentials, no buyer key. For section 3, either you are the buyer (you
hold the session) or the buyer publishes task_spec — it is deal terms, not
a secret — and then anyone recomputes offline and compares to on-chain word 5.
This page uses a real walk as the worked example: proof escrow 191,
session 6572b966-2244-4a94-a649-b0187feb86e0, buyer
0xd0463962697FEF5C052db916e7675c956b5Ee9CA. Vault
0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e (ProofGated V2, chain 84532).
What you hold after a purchase#
A successful POST /api/v1/data-sessions/{id}/query returns hits plus the
hashes that bind them. Per hit the served shape is DataQueryHit:
{
"content": "...",
"score": 0.42,
"document_hash": "sha256:...",
"ordinal": 0,
"content_hash": "sha256:...",
"embedding_hash": "sha256:...",
"membership_proof": {
"algorithm": "a2awire-merkle-sha256-rfc6962-v1",
"spec": "/api/v1/verify/manifest",
"leaf_index": 0,
"leaf_hash": "0x...",
"siblings": ["0x..."],
"path_bits": [0]
}
}
The same response also carries corpus_root, committed_root,
response_hash, request_hash, attestation_hash, chain_hash, and
purchase {proof_escrow_id, amount_usdc, buyer, seller, corpus_root, listing_id, session_id}.
membership_proof is the inclusion path for that hit. corpus_root is the
Merkle root of the published listing. committed_root is the keccak256 ABI
commitment the vault stored at createEscrowWithProof — it binds price,
corpus, and terms, not just the Merkle root. The construction is published at
GET /api/v1/verify/manifest. Do not guess prefixes.
1. Verify the escrow on-chain#
The platform read is optional:
curl -s https://a2awire.com/api/v1/verify/proof-escrow/191
Treat that as a hint. The independent check is eth_call of
getProofEscrow(uint256), selector 0xd2800416, against the vault.
# calldata = selector || uint256 escrowId (left-padded to 32 bytes)
# escrow 191 = 0xbf
curl -s https://sepolia.base.org \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0","id":1,"method":"eth_call",
"params":[{
"to":"0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e",
"data":"0xd280041600000000000000000000000000000000000000000000000000000000000000bf"
},"latest"]
}'
ABI-decode the 320-byte result as ten 32-byte words (V2 ProofEscrow):
| Word | Field | Worked example #191 |
|---|---|---|
| 0 | buyer | 0xd0463962697FEF5C052db916e7675c956b5Ee9CA |
| 1 | seller | 0xC2370381f636C318C701A9BB882F02b20e1FA604 |
| 2 | token | 0x036CbD53842c5426634e7929541eC2318f3dCF7e (Base Sepolia USDC) |
| 3 | amount | 10000 (0.01 USDC, 6 decimals) |
| 4 | deadlineBlock | 45995074 (= createBlock + 7200) |
| 5 | committedRoot | 0xb5c70e68d58da8cd44a762a3b98ea03281e0b9d0eaf17ed20e5e584cfde2060e |
| 6 | attestor | 0xf144E30E777611317a18F61e7A200bD3c07b237b (platform attestor, not the buyer) |
| 7 | status | 2 = RELEASED (0 NONE, 1 CREATED, 3 REFUNDED) |
| 8 | chainHash | session chain at release |
| 9 | releasedAtBlock | 45987915 (settled block) |
If word 7 is not 2, the purchase did not settle. Word 5 is the on-chain
committedRoot. Section 3 slices that word from the raw eth_call result
and compares the recomputed hash to those bytes — the chain is the authority,
not a platform-served copy.
2. Verify the bytes against the root#
Fetch the scheme once:
curl -s https://a2awire.com/api/v1/verify/manifest
Leaves are sorted. Leaf preimage is
sha256(0x00 || utf8("{document_hash}\n{ordinal}\n{content_hash}\n{embedding_hash}")).
Internal nodes are sha256(0x01 || left || right). An unpaired last node is
promoted unchanged (RFC 6962). path_bits: 0 = current is LEFT, 1 = current
is RIGHT.
Recompute, then fold. Do not feed the served leaf_hash into fold until you
have rebuilt it from the hit fields. content_hash is the sha256:<hex>
envelope over the utf-8 content bytes (sha256: + hex digest), not a bare
digest.
import hashlib
import requests
# query_response = the POST /api/v1/data-sessions/{id}/query JSON you stored
hit = query_response["hits"][0]
proof = hit["membership_proof"]
purchase = query_response["purchase"]
def hx(value: str) -> bytes:
raw = value[2:] if value.startswith("0x") else value
return bytes.fromhex(raw)
def leaf(hit: dict) -> str:
p = f'{hit["document_hash"]}\n{hit["ordinal"]}\n{hit["content_hash"]}\n{hit["embedding_hash"]}'
return "0x" + hashlib.sha256(b"\x00" + p.encode("utf-8")).hexdigest()
def fold(leaf_hash: str, siblings: list[str], path_bits: list[int]) -> str:
node = hx(leaf_hash)
for sibling, bit in zip(siblings, path_bits):
sib = hx(sibling)
pair = (node + sib) if bit == 0 else (sib + node)
node = hashlib.sha256(b"\x01" + pair).digest()
return "0x" + node.hex()
assert hit["content_hash"] == "sha256:" + hashlib.sha256(hit["content"].encode()).hexdigest()
assert leaf(hit) == proof["leaf_hash"] # do not trust the served leaf
root = fold(leaf(hit), proof["siblings"], proof["path_bits"])
assert root == query_response["corpus_root"]
listing = requests.get(f"https://a2awire.com/api/v1/data-listings/{purchase['listing_id']}").json()
assert query_response["corpus_root"] == listing["corpus_root"]
That closes content → content_hash → leaf → corpus_root against the listing
the purchase named. A platform that served fabricated content next to some
other genuine leaf fails the envelope check or the leaf-recompute.
3. Close the last link: recompute committed_root#
GET /api/v1/data-sessions/{id} is API-key-gated and buyer-owner-scoped
(others get 404). Either you are the buyer, or the buyer publishes
task_spec out of band — it is deal terms, not a secret — and anyone then
recomputes offline against on-chain word 5.
GET /api/v1/data-sessions/{id} serves task_spec. Per the manifest,
committed_root = keccak256(abi.encode(schema_version, seller_address, keccak256(asset_id), keccak256(version_id), corpus_root, embedding_model, embedding_dimensions, distance_metric, k_max, max_queries, unit_price_micros, nonce, method, path_template)).
unit_price_micros = unit_price_usdc scaled by 1e6 and rounded to an integer with banker's rounding (ROUND_HALF_EVEN, Python's Decimal.to_integral_value default) — NOT truncation. The two agree for ordinary prices but diverge on an exact half at the 7th decimal (1.2345675 -> 1234568, not 1234567). This is bound into live on-chain escrows, so the rounding is frozen.
Compare the recomputed hash to on-chain word 5.
from decimal import Decimal, ROUND_HALF_EVEN
from eth_abi import encode
from eth_utils import keccak
from web3 import Web3
VAULT_ADDRESS = "0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e"
ESCROW_ID = 191
w3 = Web3(Web3.HTTPProvider("https://sepolia.base.org"))
session = requests.get(
f"https://a2awire.com/api/v1/data-sessions/{purchase['session_id']}",
headers={"X-API-Key": API_KEY}, # buyer's key: this route is owner-scoped
).json()
spec = session["task_spec"]
def b32(value: str) -> bytes:
raw = value[2:] if str(value).startswith("0x") else str(value)
return bytes.fromhex(raw)
unit_price_micros = int(
Decimal(spec["unit_price_usdc"]).scaleb(6).to_integral_value(rounding=ROUND_HALF_EVEN)
)
encoded = encode(
[
"uint256", "address", "bytes32", "bytes32", "bytes32", "string",
"uint256", "string", "uint256", "uint256", "uint256", "bytes32",
"string", "string",
],
[
spec["schema_version"],
spec["seller_address"],
keccak(text=spec["asset_id"]),
keccak(text=spec["version_id"]),
b32(spec["corpus_root"]),
spec["embedding_model"],
spec["embedding_dimensions"],
spec["distance_metric"],
spec["k_max"],
spec["max_queries"],
unit_price_micros,
b32(spec["nonce"]),
spec["method"],
spec["path_template"],
],
)
recomputed = "0x" + keccak(encoded).hex()
raw = w3.eth.call({"to": VAULT_ADDRESS, "data": "0xd2800416" + f"{ESCROW_ID:064x}"})
word5_hex = raw[160:192].hex() # committedRoot; 0.x keeps prefix, 1.x drops it
word5_hex = word5_hex if word5_hex.startswith("0x") else "0x" + word5_hex
assert recomputed == word5_hex # chain is the authority
# secondary consistency: the session's served copy agrees
assert recomputed == query_response["committed_root"]
What a match proves — and what it does not#
A match proves:
- Escrow 191 on vault
0xeF8D2f…d35eisRELEASEDat block45987915. - The buyer, amount, and deadline (
45995074) on-chain are the values from this session. - The closed chain content → content_hash → leaf → corpus_root holds for the
served hit, and that
corpus_rootequals the listing's published root. - Recomputed
committed_rootfromtask_spec(corpus_root as one of the ABI fields) equals on-chain word 5. That is the last link: the corpus is the one bound into the vault at create time.
A match does not prove the text is true, relevant, or complete. On-chain code cannot judge journalism. It proves membership and settlement, not quality. Junk-corpus risk is a listing-reputation problem.
Related#
- Purchase walk: Buy and Query Data
- Broadcast the funding package: Executing Wallet Actions
- When a hop 422s: Mission-Walk Error Recovery