05 — Cross-chain funding router
The agent's wallet (Mode A on Privy, Mode B on Virtuals' Base/Solana/ARC) is rarely on the same chain a venue settles on. The funding router is the layer that makes "deposit USDC anywhere → the agent funds the venue" a first-class operation. Reference: venue_funding_router.py.
1. The chain problem, stated precisely
Agent wallet native chains: base, solana, arc (Virtuals) or wherever Privy put it
Venue settlement chains: polymarket → polygon
hyperliquid → arbitrum
tradexyz → arbitrum
pumpfun → solana
virtuals/degenclaw/bankr/base → baseSource chains a dev may want to accept (CAIP-2):
| Chain | CAIP-2 |
|---|---|
| Abstract | eip155:2741 |
| Arbitrum | eip155:42161 |
| Base | eip155:8453 |
| BNB Chain | eip155:56 |
| Ethereum | eip155:1 |
| HyperEVM | eip155:999 |
| Hypercore | eip155:1337 |
| Monad | eip155:143 |
| Optimism | eip155:10 |
| Polygon | eip155:137 |
| Solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
| Tron | tron:20191129 |
| Tempo | (emerging) |
| ARC | (Virtuals-native) |
The reference implementation's currently-wired source set (SUPPORTED_SOURCE_CHAINS): base, ethereum, bnb, solana, arbitrum, avalanche, polygon, tempo. The CAIP-2 table above is the target the design extends to — and it is exactly why we keep the chain map data-driven rather than Virtuals' 3-chain limit.
2. The three routing rails
OAW composes three funding rails, picking per venue:
Rail 1 — Chain-agnostic deposit address (Privy useDepositAddress)
Privy generates a unique deposit address; the user sends crypto from any supported source chain / any wallet / any exchange, and Privy auto-bridges/swaps to the destination wallet. This is the cleanest "fund from anywhere" rail and the reason a Privy-native dev gets cross-chain funding nearly for free. OAW surfaces it as a funding option for the agent wallet and for any venue whose target is the agent wallet's own chain.
Rail 2 — Venue-native deposit bridge (e.g. Polymarket any-chain bridge)
Some venues ship their own any-chain deposit bridge. Polymarket accepts USDC from many chains into a generated deposit address and bridges to Polygon. OAW's Polymarket route uses this directly: it generates deposit addresses, initiates no transfer itself, and tells the user "send USDC on any chain here."
Rail 3 — Cross-chain stablecoin transfer (swap/bridge engine)
When the agent already holds USDC in its wallet and the venue needs it on another chain, OAW routes through a stablecoin transfer engine (reference: stablecoin_route_service) that previews and (gated) executes a bridge/swap from the agent balance to the venue's native chain.
Rail selection (reference logic)
target_chain = VENUE_FUNDING_CHAIN[venue] # polymarket→polygon, hyperliquid→arbitrum, …
if venue == "polymarket": rail = venue_native_deposit_bridge # Rail 2
elif target_chain in SUPPORTED_SOURCE_CHAINS: rail = cross_chain_stablecoin_transfer # Rail 3
else: status = funding_route_unsupported
# Rail 1 (Privy deposit address) is offered for funding the agent wallet itself.3. Funding is two slices: resolve (read) and execute (gated)
The reference implementation is deliberately split so the read path can never move money.
Slice 1 — resolve_funding_plan(...) — read-only, moves nothing
Given (user, venue, amount_usd) returns:
{
"venue": "polymarket",
"funding_target": { "chain": "polygon", "address": "0x…" }, // from the venue's execution binding
"available_funding_usd": "37.50", // agent balance (authority ledger)
"supported_source_chains": ["base","ethereum","bnb","solana","arbitrum","avalanche","polygon","tempo"],
"route": {
"kind": "venue_native_deposit_bridge | cross_chain_stablecoin_transfer",
"target_chain": "polygon",
"accepts_any_chain_usdc": true,
"detail": "Send USDC on any supported chain to the bridge deposit address; it auto-bridges to Polygon."
},
"status": "ready | funding_required | funding_target_unresolved | funding_route_unsupported | funding_venue_unsupported",
"no_live_venue_action": true,
"secrets_exposed": false
}Statuses (verified): ready, funding_required, funding_target_unresolved, funding_route_unsupported, funding_venue_unsupported.
Slice 2 — execute_funding(...) — gated, fail-closed money movement
Behavior by rail:
- Polymarket →
create_deposit_addresses(any-chain deposit addresses). No OAW-initiated transfer; statusdeposit_addresses_ready. The user (or an upstream wallet) sends; the bridge delivers. - Cross-chain venues →
stablecoin_route_service:policy_decision(kill-switch / max-transfer / daily-cap / destination-chain validation) →preview_route. An actual transfer happens only when all hold:execute=Trueand explicitapprovaland the fail-closed config flagVENUE_FUNDING_EXECUTE_ENABLED. Otherwiseready_but_live_locked/requires_approval,execution_performed: false.
Pre-bind authority is required first — funding never auto-creates a wallet (funding is not a bind; if there's no authority it returns agent_wallet_required).
4. Where the funding target comes from
The venue's funding target (chain + address) is not invented by the router; it is read from the venue's execution binding (the venue adapter's funding_target() / the execution-binding builder). This keeps a single source of truth: the address you fund is the address the venue will actually trade from.
funding_target = execution_binding(venue).funder_address on VENUE_FUNDING_CHAIN[venue]
available = authority.available_funding_usd (the agent balance ledger)5. The agent-authority-aware source resolution
The router resolves the source wallet from the parent authority — Mode A (Privy address) or Mode B (Virtuals address) — via the agent_authority block (06). This is why funding "just works" regardless of mode: the source is whatever the active authority's wallet is, on whatever chain it's on, and the rails bridge to the target.
6. Surfaces
The router is callable three ways (reference):
- Service:
VenueFundingRouter.resolve_funding_plan(...)/execute_funding(...). - REST:
POST /api/venues/{venue}/funding/route(resolve) andPOST /api/venues/{venue}/funding/execute(gated). - Chat: "fund my Polymarket with $50" → reasoning classifier (06) → preview by default; "confirm / send it" → gated execute. Negations/questions ("should I fund Hyperliquid?") never trigger a transfer.
7. What L3 gives a dev
plan = await oaw.funding.resolve(user, "polymarket", amount_usd=50)
# plan.route.kind == "venue_native_deposit_bridge"; plan.status == "funding_required"
res = await oaw.funding.execute(user, "polymarket", amount_usd=50, execute=False)
# → deposit addresses; no funds movedMoney is on the right chain. L4 (06) makes execution authority-aware and safe.