[requires: http]
GET /api/v1/data-sessions/{id}/funding-package is the spend surface. It does
not broadcast. You sign. The platform never holds wallet_private_key.
This page maps the served JSON onto a correct broadcast loop, then names the library quirks that stopped a live buy-the-news walk even when the calldata was right.
The shape is a dict, not a list#
wallet_actions is an object:
{
"type": "wallet_actions",
"chain_id": 84532,
"account": "0xd0463962697FEF5C052db916e7675c956b5Ee9CA",
"execution_policy": {
"mode": "sequential_confirmed",
"wait_between_actions": true,
"rules": ["..."]
},
"actions": [
{"id": "approve_usdc", "depends_on": [], "tx": {"to": "...", "data": "0x...", "value": "0", "chainId": 84532},
"wait_for": {"type": "receipt_and_eth_call", "check": "allowance_gte_amount",
"owner": "0x...", "spender": "0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e",
"amount": "10000", "min_confirmations": 1}},
{"id": "createEscrowWithProof", "depends_on": ["approve_usdc"],
"tx": {"to": "0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e", "data": "0x...", "value": "0", "chainId": 84532},
"result": {"capture": "tx_hash", "then": "attach_with_open_tx_hash"},
"retry": {"max": 3, "backoff_seconds": [2, 4, 8], "recheck": "allowance_ok",
"on_failure": "do_not_reapprove"}}
]
}
Iterate wallet_actions["actions"]. Do not treat wallet_actions itself as
the list. execution_policy.mode is sequential_confirmed: finish action N
(including its wait_for) before estimating gas for N+1.
Top-level siblings: progress (allowance_ok, eth_balance_ok,
create_tx_hash), gas_prerequisite, signing_instructions (rpc_url,
broadcast_method: eth_sendRawTransaction), and steps[] (skip_if,
depends_on).
Order of operations#
- If
gas_prerequisiteis present, POST it first. Body is usually{"address":"<buyer>"}(ETH gas). Onfaucet_cooldown, retry withrecovery_on_cooldown.body(reason: mission_topup). - If
progress.allowance_okis true, or the approve step'sskip_ifisallowance_okand already satisfied, skipapprove_usdc. - Else broadcast
approve_usdc, then honorwait_for.check = allowance_gte_amount: receipt.status == 1 andallowance(owner, spender) >= amount. A single low read after a successful receipt is usually node lag. Re-read with 2s/4s/8s backoff. Never re-approve on one stale read. - Only then
estimateGas+ broadcastcreateEscrowWithProof(depends_on: [approve_usdc]). 4b. On create revert or estimate failure: re-read allowance; if ok, retry create up to 3 times with 2s/4s/8s backoff (retry.max,backoff_seconds). Do not re-approve (on_failure: do_not_reapprove). The approve already landed; a second approve is the failure mode this page exists to prevent. - Capture
tx_hash. POST attach-escrow with{"open_tx_hash": tx_hash}. Attach-escrow gates onstartswith("0x") and len == 66; a 64-char unprefixed hash keeps the walk emittingblocked_reason: wallet_actions_required.
Vault (V2): 0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e. USDC (Base Sepolia):
0x036CbD53842c5426634e7929541eC2318f3dCF7e. RPC: https://sepolia.base.org.
Sign and broadcast (Python)#
from eth_account import Account
from web3 import Web3
from web3.exceptions import TransactionNotFound
w3 = Web3(Web3.HTTPProvider("https://sepolia.base.org"))
account = Account.from_key(WALLET_PRIVATE_KEY) # from onboard; never POST this
def raw_hex(signed) -> str:
raw = signed.raw_transaction # bytes on some versions, hex str on others
if isinstance(raw, (bytes, bytearray)):
return "0x" + raw.hex()
return raw if raw.startswith("0x") else "0x" + raw
def tx_hash_hex(h) -> str:
s = h.hex() if hasattr(h, "hex") else str(h) # 0.x keeps '0x', 1.x drops it
return s if s.startswith("0x") else "0x" + s
def send_action(tx_spec: dict) -> str:
estimate = w3.eth.estimate_gas({
"from": account.address,
"to": tx_spec["to"],
"data": tx_spec["data"],
"value": int(tx_spec.get("value") or 0),
})
gas_price = w3.eth.gas_price
typed = {
"type": "0x2", # NOT "eip1559" — eth_account rejects the name
"chainId": 84532,
"nonce": w3.eth.get_transaction_count(account.address, "pending"),
"to": tx_spec["to"],
"data": tx_spec["data"],
"value": int(tx_spec.get("value") or 0),
"gas": int(estimate * 1.3),
"maxFeePerGas": gas_price * 2,
"maxPriorityFeePerGas": w3.to_wei(0.001, "gwei"),
}
signed = account.sign_transaction(typed)
tx_hash = w3.eth.send_raw_transaction(raw_hex(signed))
return tx_hash_hex(tx_hash)
def wait_receipt(tx_hash: str, timeout=180):
import time
deadline = time.time() + timeout
while time.time() < deadline:
try:
receipt = w3.eth.get_transaction_receipt(tx_hash)
if receipt is not None:
return receipt
except TransactionNotFound:
pass # pending: web3 raises; it does not return None
time.sleep(2)
raise TimeoutError(tx_hash)
Broadcast path if you skip the SDK: eth_sendRawTransaction with the hex from
raw_hex(signed). That is signing_instructions.broadcast_method.
Library quirks (all hit live)#
"0x2" not "eip1559". eth_account.sign_transaction (0.13+) wants the
EIP-1559 type as the hex literal "0x2". Passing "eip1559" raises. Type 2
(int) is accepted by some versions; "0x2" is the portable form.
TransactionNotFound while pending. web3.py get_transaction_receipt
raises TransactionNotFound until the tx is mined. A while receipt is None
loop never sees None. Catch the exception in every poll.
signed.raw_transaction is bytes or hex. eth_account / web3 v6 vs v7 vs v8
disagree. If it is bytes, hex it ("0x" + raw.hex()) before
eth_sendRawTransaction. If it is already a 0x string, send it as-is.
Tx hash: normalize with a startswith("0x") check. hexbytes 0.x
HexBytes.hex() keeps the 0x prefix; hexbytes 1.x returns the bare
hex. to_0x_hex() only exists on 1.2+. Prefixing blindly ("0x" + tx_hash.hex()) double-prefixes on 0.x (68 chars). Use tx_hash_hex()
above. Attach-escrow gates on startswith("0x") and len == 66.
Gas heuristics that worked on Base Sepolia. gas = estimate * 1.3,
maxFeePerGas = gas_price * 2, maxPriorityFeePerGas = 0.001 gwei. Underpaying
priority fee leaves the create tx parked while the walk times out.
Gas drip recovery#
When progress.eth_balance_ok is false the package includes
gas_prerequisite (id: faucet_drip, when: before_spend). POST
/api/v1/faucet/drip with the served body (buyer address). If you already
dripped and hit cooldown, POST recovery_on_cooldown.body
(reason: mission_topup) for one rescue ETH drip. Then re-read
/api/v1/faucet/status (status_path) before signing.
Related#
- The spend walk that produces this package: Buy and Query Data
- After attach + query: Verify a Data Purchase On-Chain
- Hop 422 / 409 / MCP 400: Mission-Walk Error Recovery