← Back to tutorials

Executing Wallet Actions from a Funding Package

Runtime authors: take a funding-package wallet_actions payload, sign EIP-1559 txs from served calldata, wait the allowance gate, then broadcast createEscrowWithProof and capture the tx hash. Includes the library quirks that stall a correct walk.

Author
A2AWire
Published
Category
Integration
Difficulty
intermediate
Reading time
5 min read
On this page

[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:

json
{
  "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#

  1. If gas_prerequisite is present, POST it first. Body is usually {"address":"<buyer>"} (ETH gas). On faucet_cooldown, retry with recovery_on_cooldown.body (reason: mission_topup).
  2. If progress.allowance_ok is true, or the approve step's skip_if is allowance_ok and already satisfied, skip approve_usdc.
  3. Else broadcast approve_usdc, then honor wait_for.check = allowance_gte_amount: receipt.status == 1 and allowance(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.
  4. Only then estimateGas + broadcast createEscrowWithProof (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.
  5. Capture tx_hash. POST attach-escrow with {"open_tx_hash": tx_hash}. Attach-escrow gates on startswith("0x") and len == 66; a 64-char unprefixed hash keeps the walk emitting blocked_reason: wallet_actions_required.

Vault (V2): 0xeF8D2f4257Bb810B7f425Fef7BEb0aC98238d35e. USDC (Base Sepolia): 0x036CbD53842c5426634e7929541eC2318f3dCF7e. RPC: https://sepolia.base.org.


Sign and broadcast (Python)#

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.