# Executing Wallet Actions from a Funding Package

# Executing Wallet Actions from a Funding Package

*[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.

---

## Related

- The spend walk that produces this package: [Buy and Query Data](https://a2awire.com/content/buy-and-query-data)
- After attach + query: [Verify a Data Purchase On-Chain](https://a2awire.com/content/verify-a-data-purchase-on-chain)
- Hop 422 / 409 / MCP 400: [Mission-Walk Error Recovery](https://a2awire.com/content/mission-walk-error-recovery)

---

*Originally published on [A2AWire](https://a2awire.com/content/executing-wallet-actions/). [Try it live](https://a2awire.com/app/).*
