04 — Venue binding & the universal pre-bind gate
L1 gave the agent one authority. L2 binds that authority to many venues — declaratively, so adding a venue is data + one adapter, not a rewrite.
1. The venue capability contract (declarative registry)
Every venue is described once, as data, in a registry (reference: venue_capability_contract.py). This is the single source of truth for how a venue connects, funds, and executes. The actual field set (verified):
@dataclass(frozen=True)
class VenueCapabilityContract:
venue_id: str
label: str
aliases: tuple[str, ...]
supported_intents: tuple[str, ...] # trade, fund, status, ...
supported_market_classes: tuple[str, ...] # perp, spot, prediction, token, ...
lifecycle_modes: tuple[LifecycleMode, ...] # create_account, link_existing, ...
auth_model: str # economyos_agent_wallet_plus_*, cex_api_key, ...
signing_model: str # privy_agent_wallet, eip1271, api_wallet, ...
per_user_authority_required: bool # ← does the pre-bind gate apply?
managed_wallet_support: bool
delegated_signer_support: bool
browser_session_required: bool # owner/OAuth/dashboard handoff needed?
funding_requirements: tuple[str, ...]
readiness_requirements: tuple[str, ...]
approval_requirements: tuple[str, ...]
reconciliation_supported: bool
geo_compliance_gates: tuple[str, ...] = ()
# UX + planner hints
setup_copy: str = ""
readiness_copy: str = ""
planner_trade_step: str | None = None
planner_scan_step: str | None = None
mission_hints: tuple[str, ...] = ()
requires_trade_side: bool = True
# binding surface
binding_supported: bool = False
binding_required_fields: tuple[str, ...] = ()
binding_runtime_env_hints: tuple[str, ...] = ()
binding_example: str | None = None
# taxonomy
profile_name: str = "user_default"
profile_kind: str | None = None
binding_kind: str | None = None
role: str | None = NoneWhy declarative matters
A dev adds Kalshi (say) by adding one contract entry — not by touching the gate, the funding router, or the chat layer. The contract's fields drive:
- whether the pre-bind gate runs (
per_user_authority_required), - whether it's ACP-native (derived:
auth_modelstarts witheconomyos_agent_wallet_plus_and venue ∈ ACP-native set), - what funding/readiness/approval gates apply,
- whether the venue is even actionable yet (
binding_supported).
Lesson burned in (the kalshi bug): flipping
binding_supported=Trueprematurely routed a "coming soon" venue into live read/DB paths and crashed. A venue'sbinding_supportedflag is a promise the backend can actually service it. Keep itFalseuntil the adapter exists. The contract is a contract.
2. The reference venue catalog
| Venue | auth_model (shape) | signing_model | Native chain | ACP-native | Notes |
|---|---|---|---|---|---|
| virtuals | economyos_agent_wallet_plus_acp | virtuals_acp_sidecar | base/solana/arc | ✅ | The native EconomyOS/ACP layer + a venue. |
| degenclaw | economyos_agent_wallet_plus_acp | virtuals_acp_sidecar | arbitrum (HL under hood) | ✅ | Virtuals-routed perps/spot via Hyperliquid. |
| hyperliquid | agent_wallet_plus_api_wallet | api_wallet / approveAgent | arbitrum | — | perps/spot/HIP-3/4. |
| polymarket | agent_wallet_plus_deposit | eip1271 + clob_creds | polygon | — | any-chain USDC deposit bridge; pUSD. |
| pumpfun | managed_solana_wallet | solana_managed | solana | — | memecoins. |
| bankr | agent_wallet | base_account | base | — | Base-native. |
| binance | cex_api_key | api_key | bnb | — | CEX. |
| tradexyz | agent_wallet_plus_routeproof | route_proof | arbitrum | — | route-proof binding. |
| kalshi | (coming soon) | — | — | — | binding_supported=False. |
Each row is data. The only code per venue is the adapter's execute() (and any venue-specific setup, e.g. Polymarket CLOB derivation).
3. The universal pre-bind gate
This is the keystone. No venue binding, signer, deposit wallet, or credential is created until parent authority is resolved. Reference: venue_prebind_gate.py (full module is small; read it).
ANY venue bind / setup / fund / execute
│
▼
pre-bind gate ── resolve_authority(user, venue, action, allow_auto_create)
│
┌────┴───────────────────────────────────────────────┐
│ ok (authority_ready | agent_wallet_created) │ → proceed to venue setup
│ blocked → exact status, NO venue artifacts created │ → return blocker
└─────────────────────────────────────────────────────┘The status taxonomy (never collapse into a generic signer_required)
| Status | Meaning | ok |
|---|---|---|
authority_ready | Parent authority resolved, venue can proceed | ✅ |
agent_wallet_created | Mode A auto-created this call, proceed | ✅ |
agent_wallet_required | No authority; read path or auto-create disabled | ❌ |
virtuals_identity_required | ACP-native venue needs a linked Virtuals identity | ❌ |
virtuals_signer_required | Virtuals identity present but signer not ready | ❌ |
owner_handoff_required | Pending Virtuals link needs an owner action | ❌ |
authority_reconciliation_required | wallet_provider=virtuals but proof not verified | ❌ |
unsupported_authority_source | Provider not in | ❌ |
venue_contract_missing | Venue not in the registry | ❌ |
Every blocked result carries a safe user message, a next action, and secrets_exposed: false. Precise blockers are a product feature: the chat surface can tell the user exactly what to do ("link your Virtuals agent", "approve the signer") instead of a generic "failed".
allow_auto_create — the write/read distinction
The single most important parameter. It encodes intent:
- Write/setup intent (the user said "bind X", or hit a setup endpoint):
allow_auto_create=True→ Mode A is minted if no authority exists. - Read/status/passive (status checks, market search, portfolio reads, background polling, GETs):
allow_auto_create=False→ returnsagent_wallet_requiredrather than silently creating a wallet.
This is what stops a stray status poll from provisioning wallets, while still letting "Bind Polymarket" do the full setup with zero extra user steps.
4. The bind lifecycle
explicit bind/setup intent (chat NL → classifier, or slash cmd, or REST)
→ pre-bind gate (allow_auto_create=True)
→ Mode A auto-created if needed | Mode B honored if proof verified
→ create/load VenueBinding (child of AgentAuthority)
→ venue-specific setup (adapter): deposit wallet / API wallet / CLOB / route-proof
→ readiness checks (funding, approvals, signer)
→ ready (or ready_but_live_locked if live flags off)
→ append audit event (binding_payload JSONB; pre-trade lifecycle)Separation of concerns in the ledger (keep these distinct):
- Binding lifecycle events →
VenueBinding.binding_payload+ binding audit. Not spend events (no money moved yet). - Spend/action events →
SpendIntent+ spend events, only once a durable spend intent exists. - Order/execution receipts →
ExecutionReceipt/EconomicActionReceipt.
5. Venue adapter interface
A venue adapter is the only code a dev writes per venue. It implements (full signatures in 07):
class VenueAdapter:
contract: VenueCapabilityContract
async def setup(authority, binding, params) -> SetupResult # provision venue-local artifacts
async def readiness(authority, binding) -> ReadinessResult # funding/approval/signer checks
async def funding_target(authority, binding) -> FundingTarget # chain + address to fund
async def execute_or_plan(authority, binding,
action, *, execute: bool) -> ExecutionResult # preflight + (maybe) executeRules every adapter must obey:
setupruns only after the pre-bind gate passed.- Venue-local signers/credentials (Polymarket CLOB creds, Hyperliquid API wallet, Pump.fun Solana wallet) are child artifacts, never parent authority.
execute_or_plan(execute=False)does the full preflight and returns a plan, signs nothing, moves nothing.execute=Trueruns the same preflight, then the policy/funding/live-flag gates, then executes — or returns the exact blocker.- Never return secrets.
6. Many venues under one authority (a hard requirement)
AgentAuthority
├─ VenueBinding(polymarket)
├─ VenueBinding(hyperliquid)
├─ VenueBinding(virtuals)
└─ VenueBinding(pumpfun)The authority object must support many binding children. (Reference note: a single-primary venue_binding_id field is legacy; do not let one venue overwrite another's link. Bindings are a collection keyed by (user, agent, venue).) This is what makes "show me all my open positions across venues" and "bind three venues" possible under one wallet.
7. What L2 gives a dev
binding = await oaw.venues.bind(user, "polymarket", params, allow_auto_create=True)
# → pre-bind gate ran, Mode A minted if needed, deposit wallet + CLOB set up
status = await oaw.venues.readiness(user, "polymarket") # funding/approval gatesThe venue is bound and authority-correct. L3 (05) makes sure it has money on the right chain.