Skip to content

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

python
@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 = None

Why 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_model starts with economyos_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=True prematurely routed a "coming soon" venue into live read/DB paths and crashed. A venue's binding_supported flag is a promise the backend can actually service it. Keep it False until the adapter exists. The contract is a contract.

2. The reference venue catalog

Venueauth_model (shape)signing_modelNative chainACP-nativeNotes
virtualseconomyos_agent_wallet_plus_acpvirtuals_acp_sidecarbase/solana/arcThe native EconomyOS/ACP layer + a venue.
degenclaweconomyos_agent_wallet_plus_acpvirtuals_acp_sidecararbitrum (HL under hood)Virtuals-routed perps/spot via Hyperliquid.
hyperliquidagent_wallet_plus_api_walletapi_wallet / approveAgentarbitrumperps/spot/HIP-3/4.
polymarketagent_wallet_plus_depositeip1271 + clob_credspolygonany-chain USDC deposit bridge; pUSD.
pumpfunmanaged_solana_walletsolana_managedsolanamemecoins.
bankragent_walletbase_accountbaseBase-native.
binancecex_api_keyapi_keybnbCEX.
tradexyzagent_wallet_plus_routeproofroute_proofarbitrumroute-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)

StatusMeaningok
authority_readyParent authority resolved, venue can proceed
agent_wallet_createdMode A auto-created this call, proceed
agent_wallet_requiredNo authority; read path or auto-create disabled
virtuals_identity_requiredACP-native venue needs a linked Virtuals identity
virtuals_signer_requiredVirtuals identity present but signer not ready
owner_handoff_requiredPending Virtuals link needs an owner action
authority_reconciliation_requiredwallet_provider=virtuals but proof not verified
unsupported_authority_sourceProvider not in
venue_contract_missingVenue 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 → returns agent_wallet_required rather 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 eventsVenueBinding.binding_payload + binding audit. Not spend events (no money moved yet).
  • Spend/action eventsSpendIntent + spend events, only once a durable spend intent exists.
  • Order/execution receiptsExecutionReceipt / 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) execute

Rules every adapter must obey:

  • setup runs 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=True runs 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

python
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 gates

The venue is bound and authority-correct. L3 (05) makes sure it has money on the right chain.

Released under the Apache-2.0 License.