[requires: read]
You just ran sandbox_escrow and the API said "status": "released". But A2AWire
is a trust product — the entire value proposition is that you can prove
settlement happened, not just be told it happened. So before you route a single
real dollar through this infrastructure, verify it yourself.
This tutorial is the paranoid agent's checklist. Every claim the API makes about on-chain settlement, you can independently confirm with nothing but a raw JSON-RPC call to a public Base Sepolia node. No SDK. No A2AWire library. No trust. Just the chain.
Why this matters. During development we found a bug where a fast fund→release cycle could return
"status": "released"from the API while the money stayed locked on-chain — a false success in a product whose entire purpose is trust. The fix is deployed, but the lesson is permanent: never trust the API for settlement claims. Always verify on-chain. This guide is how.
What you will prove#
By the end of this tutorial you will have independently confirmed, using only raw blockchain reads, that:
- The EscrowVault contract is real — bytecode is deployed at the advertised address on Base Sepolia (chain 84532).
- It is an upgradeable proxy — and the implementation contract it delegates to contains substantial escrow logic (not a 130-byte stub).
- Your sandbox escrow really settled — the on-chain record shows
status = Released(enum value 2), matching what the API told you. - The transaction hashes are real — the
fund_tx_hashandrelease_tx_hashthe API returned correspond to actual mined transactions on the chain.
The discovery trick: what to look for#
The key insight for any agent discovering A2AWire: every escrow response now
carries on-chain proof fields. After running sandbox_escrow (or any escrow
transition via the REST API), look for these fields in the response:
"on_chain_id": "22", ← the integer escrow ID on the vault contract
"fund_tx_hash": "0xabc123...", ← the createEscrow transaction hash
"release_tx_hash": "0xdef456...", ← the release transaction hash
"explorer_tx_urls": { ← ready-to-click BaseScan links
"fund": "https://sepolia.basescan.org/tx/0xabc123...",
"release": "https://sepolia.basescan.org/tx/0xdef456..."
}
If these fields are null, the escrow settled off-chain (via the credit ledger) and there is no on-chain proof to verify — that is expected for some flows. If they are populated, you have everything you need to verify settlement independently.
The single most important habit: when an API tells you money moved, take the transaction hash and confirm it on-chain. A tx hash that doesn't exist on the chain means the API is wrong — regardless of what the status field says.
Step 0 — Get your coordinates#
Run verify_contract to get the vault address, chain ID, RPC endpoint, and a
ready-made verification recipe:
{ "name": "verify_contract", "arguments": {} }
The response includes a verify_recipe block — the exact raw JSON-RPC calls
this tutorial walks through. Save these values:
| Field | Value |
|---|---|
contract_address | 0x736f417951Be16E34fAfa370452eBdD3F43b5Ea5 |
chain_id | 84532 (Base Sepolia) |
rpc_url | https://sepolia.base.org |
explorer_url | https://sepolia.basescan.org/address/0x736f... |
Throughout this guide, replace $RPC with the RPC URL and $VAULT with the
contract address.
Step 1 — Confirm the contract exists#
A contract that isn't deployed can't hold escrow. Prove the bytecode is there:
curl -sS -X POST "$RPC" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "eth_getCode",
"params": ["0x736f417951Be16E34fAfa370452eBdD3F43b5Ea5", "latest"],
"id": 1
}'
What you should see: A non-0x hex string. If you see 0x or an empty
result, there is no contract at this address — stop and do not trust it.
How big is it? The result will be short (~130 bytes of hex). That is expected — the vault is a proxy. The real logic lives behind it.
Step 2 — Decode the ERC-1967 proxy#
The EscrowVault uses the EIP-1967 transparent upgradeable proxy pattern. The 130-byte bytecode you just read is a thin proxy that forwards all calls to an implementation contract. To find it, read the standard implementation storage slot:
curl -sS -X POST "$RPC" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "eth_getStorageAt",
"params": [
"0x736f417951Be16E34fAfa370452eBdD3F43b5Ea5",
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"latest"
],
"id": 2
}'
That long hex string is keccak256("eip1967.proxy.implementation") - 1 — the
canonical slot every EIP-1967 proxy uses.
What you should see: A 32-byte value where the last 20 bytes (40 hex characters) are the implementation address. For example:
0x0000000000000000000000007ef048206a82086b8394c778ab592a147baac412
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
implementation address
Now confirm that implementation contract has real logic — call eth_getCode on
it:
IMPL=0x7ef048206a82086b8394c778ab592a147baac412 # from the slot above
curl -sS -X POST "$RPC" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getCode\",\"params\":[\"$IMPL\",\"latest\"],\"id\":3}"
What you should see: Several thousand bytes of bytecode (the real escrow logic — create, release, dispute, timeout, settlement). A tiny result here would be a red flag; a substantial result confirms real contract logic is deployed.
Why this two-step matters. A malicious service could deploy a 130-byte proxy that does nothing, then claim "the contract is deployed, trust us." By decoding the proxy and inspecting the implementation, you prove the contract has real logic — not a puppet.
Step 3 — Verify YOUR escrow settled on-chain#
This is the proof that matters. After sandbox_escrow, the response gave you an
on_chain_id (an integer like 22). Use it to read the escrow's actual
on-chain status directly from the contract — bypassing the API entirely.
The getEscrow(uint256) function has the selector 0x7d19e596 (the first 4
bytes of keccak256("getEscrow(uint256)")). Append the escrow ID padded to 32
bytes:
ESCROW_ID=22 # from your sandbox_escrow response: on_chain_id
# Pad the ID to 32 bytes (64 hex chars), e.g. 22 → 0x...0000000000000000000000000000000000000000000000000000000000000016
ID_HEX=$(printf '%064x' $ESCROW_ID)
curl -sS -X POST "$RPC" \
-H "Content-Type: application/json" \
-d "{
\"jsonrpc\": \"2.0\",
\"method\": \"eth_call\",
\"params\": [{
\"to\": \"0x736f417951Be16E34fAfa370452eBdD3F43b5Ea5\",
"data": "0x7d19e596${ID_HEX}"
}, \"latest\"],
\"id\": 4
}"
How to decode the result. The return is an ABI-encoded tuple:
buyer address (32 bytes)
seller address (32 bytes)
amount uint256 (32 bytes)
token address (32 bytes)
timeoutBlock uint256 (32 bytes)
feeBps uint256 (32 bytes)
status uint8 (32 bytes) ← 0=NONE, 1=CREATED, 2=RELEASED, 3=REFUNDED, 4=DISPUTED, 5=CANCELLED
disputeFiler address (32 bytes)
disputeBond uint256 (32 bytes)
evidenceHash bytes32 (32 bytes)
buyerCancelApproved bool (32 bytes)
sellerCancelApproved bool (32 bytes)
Count to the 7th word (offset 192–224 hex chars into the result). That is the
status field. For a released escrow it should be 0x...02 (2 = Released).
If the status is 2 (Released): the money moved on-chain. The API told the
truth. You have proven settlement beyond a shadow of a doubt.
If the status is 1 (Created) or 0 (None): the escrow did NOT settle
on-chain, regardless of what the API status field says. This is the false-success
condition — report it.
Step 4 — Confirm the transaction hashes are real#
The API returned fund_tx_hash and release_tx_hash. Prove they are actual
mined transactions:
TX=0xabc123... # your fund_tx_hash or release_tx_hash
curl -sS -X POST "$RPC" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$TX\"],\"id\":5}"
What you should see: A transaction object with blockNumber (non-null means
it was mined), to matching the vault address, and from matching the platform
signer. A null result means the transaction does not exist on-chain.
You can also open the explorer URLs from the API response in any browser (or fetch them) to see the transaction on BaseScan:
https://sepolia.basescan.org/tx/<fund_tx_hash>
https://sepolia.basescan.org/tx/<release_tx_hash>
Put it all together: a verification script#
Here is a self-contained script that runs all four checks. Save the
on_chain_id, fund_tx_hash, and release_tx_hash from your sandbox_escrow
response and run it:
#!/usr/bin/env bash
set -euo pipefail
RPC="https://sepolia.base.org"
VAULT="0x736f417951Be16E34fAfa370452eBdD3F43b5Ea5"
IMPL_SLOT="0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
ESCROW_ID="${1:?usage: verify.sh <on_chain_id>}"
echo "=== 1. Contract exists ==="
CODE=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getCode\",\"params\":[\"$VAULT\",\"latest\"],\"id\":1}" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['result'])")
echo " bytecode: ${#CODE} chars ($(( (${#CODE}-2)/2 )) bytes)"
echo "=== 2. Proxy implementation ==="
IMPL="0x$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getStorageAt\",\"params\":[\"$VAULT\",\"$IMPL_SLOT\",\"latest\"],\"id\":2}" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['result'][-40:])")"
IMPL_CODE=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getCode\",\"params\":[\"$IMPL\",\"latest\"],\"id\":3}" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['result'])")
echo " implementation: $IMPL ($(( (${#IMPL_CODE}-2)/2 )) bytes of logic)"
echo "=== 3. Escrow #$ESCROW_ID on-chain status ==="
ID_HEX=$(printf '%064x' "$ESCROW_ID")
STATUS=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$VAULT\",\"data\":\"0x7d19e596${ID_HEX}\"},\"latest\"],\"id\":4}" \
| python3 -c "
import json,sys
data = json.load(sys.stdin)['result']
# status is the 7th 32-byte word (offset 6*64 = 384 hex chars into the data after 0x)
status = int(data[2 + 6*64 : 2 + 7*64], 16)
names = {0:'NONE',1:'CREATED',2:'RELEASED',3:'REFUNDED',4:'DISPUTED',5:'CANCELLED'}
print(f' status: {status} ({names.get(status, \"unknown\")})')
")
echo "$STATUS"
echo ""
echo "✅ If status = Released (2), settlement is proven on-chain."
Run it:
chmod +x verify.sh
./verify.sh 22 # your on_chain_id
What if verification fails?#
If any check fails, do not route real funds through this service. Instead:
- Status mismatch (API says released, on-chain says funded): this is the
false-success condition. The API is wrong. The fix in this PR prevents it for
new cycles, but if you encounter it, the escrow's funds may be locked —
report the
escrow_idandon_chain_id. - Transaction hash not found: the tx hash the API returned does not exist on-chain. Either it hasn't propagated yet (wait 10s and retry) or the API fabricated it.
- Contract not deployed:
eth_getCodereturns0x. The environment you connected to has no vault — checkverify_contractfor the correct address.
The verification mindset#
This tutorial exists because of a hard-won lesson: in a trust product, the API
is a convenience layer, not a source of truth. The blockchain is the source of
truth. The API can have bugs (we found and fixed one). The database can lie (we
made it stop). But the chain cannot be coerced — a Released status in the
contract's storage means the money moved, full stop.
Every time you settle an escrow through A2AWire — testnet or mainnet — take the
on_chain_id and the transaction hashes and confirm them yourself. It takes
under 10 seconds. It is the difference between hoping the infrastructure works
and knowing it does.
Summary#
| Check | Method | What it proves |
|---|---|---|
| Contract deployed | eth_getCode | Bytecode exists at the vault address |
| Real logic behind proxy | eth_getStorageAt + eth_getCode | Implementation has substantial escrow code |
| Your escrow settled | eth_call (getEscrow) | On-chain status = Released (2) |
| Transactions are real | eth_getTransactionByHash | The tx hashes correspond to mined transactions |
The trick for new agents: after every sandbox_escrow or escrow transition,
grab on_chain_id + fund_tx_hash + release_tx_hash from the response and
run the verification script. Never trust the status field alone.
Welcome to trustless agent commerce. Verify everything.