asyncapi: 3.0.0
info:
  title: Triport — Wallet Watch events
  version: 0.2.0-wa3
  description: |
    Push delivery of wallet-watch events for one subscription created with
    `POST /v1/wallet/watch` (`delivery.type: "ws"`). Events are produced by
    Triport's per-network block scanner (Ethereum, Base, Polygon, BNB Smart
    Chain, Robinhood Chain): contract and smart-wallet deployments, bridge
    deposits and fills (Across, Stargate v2, deBridge DLN, Base/Polygon/
    Robinhood native bridges, Relay solvers), fresh-wallet funding,
    EIP-7702 delegations, and `wallet_activity` — every transaction in which
    an address of the subscription's watchlist takes part (one event per
    network, transaction and watched address; guide:
    `docs/api/guide/platform/wallet-activity.md`). Each participant of the
    other event types can be enriched with the cross-chain profile from
    `GET /v1/wallet/{address}/identity`.

    Every delivered event carries `event_id` (stable across PoPs and
    redeliveries — deduplicate by it) and `cursor` (opaque, `c1.…`, bound to
    the subscription). Reconnect with `?cursor=<last cursor>`: on the same PoP
    the replay continues exactly by seq; on the other PoP it continues by
    block numbers and up to about one block per network can repeat. Events
    are stored per PoP for 72 hours (5 GiB store cap); an older cursor gets a
    `cursor_expired` frame and close code `4410`. Events of one network
    arrive in order; order across networks is not guaranteed.

    System events are delivered to the subscription regardless of its
    selected types: `reorg_revert` (with the ids of this subscription's
    removed events) and `gap` (events of a block range will never be
    delivered, `recoverable: false`).

    Business tier and above (`wallet_identity` category); the subscription
    must belong to the API key's account.
defaultContentType: application/json
servers:
  production: {$ref: '_shared.yaml#/servers/production'}
channels:
  /ws/wallet-watch:
    address: /ws/wallet-watch
    description: |
      One subscription per socket: `?id=<subscription id>` is required and
      must be a subscription of the calling account with `delivery.type` =
      `ws`. Optional `?cursor=<cursor>` (preferred) or the deprecated
      `?after_seq=N` (PoP-local seq) replays stored events first; then a
      `resume` control frame; then live delivery. Without either, delivery
      is live only (`resume.mode: "none"`).

      Rejected before the upgrade with HTTP `400`: `invalid_cursor`
      (malformed or longer than 4096 characters),
      `cursor_subscription_mismatch` (cursor issued for another
      subscription), `invalid_after_seq`.

      No client → server messages are needed; the server pings every 25 s.
      Close codes: `4410` after a `cursor_expired` frame (the cursor is older
      than the retained window — resume from `oldest_cursor` or connect
      without a cursor); `4029` when the client does not drain its buffer
      (1024 frames) — reconnect with the last cursor.
    parameters:
      id:
        description: Subscription id (UUID) from `POST /v1/wallet/watch`.
      cursor:
        description: The `cursor` of the last event processed. Takes precedence over `after_seq`.
      after_seq:
        description: 'Deprecated: replay events with a PoP-local `seq` greater than this value first.'
    messages:
      resume: {$ref: '#/components/messages/resume'}
      cursorExpired: {$ref: '#/components/messages/cursorExpired'}
      walletActivity: {$ref: '#/components/messages/walletActivity'}
      event: {$ref: '#/components/messages/event'}
    x-tier-required: business
    x-tier-category: wallet_identity
    x-scope: "*"
operations:
  receiveWalletWatch:
    action: receive
    channel: {$ref: '#/channels/~1ws~1wallet-watch'}
    summary: Receive wallet-watch events for one subscription.
    messages:
      - {$ref: '#/channels/~1ws~1wallet-watch/messages/resume'}
      - {$ref: '#/channels/~1ws~1wallet-watch/messages/cursorExpired'}
      - {$ref: '#/channels/~1ws~1wallet-watch/messages/walletActivity'}
      - {$ref: '#/channels/~1ws~1wallet-watch/messages/event'}
components:
  messages:
    resume:
      name: resume
      title: Resume control frame
      summary: Sent after the replay (if any), before live delivery.
      payload:
        type: object
        required: [type, subscription, after_seq, mode, replayed, buffer_complete, cursor]
        properties:
          type: {type: string, const: resume}
          subscription: {type: string}
          after_seq: {type: integer, description: The `after_seq` given on connect (0 if none).}
          mode:
            type: string
            enum: [none, seq, blocks]
            description: '`none` = live only; `seq` = exact replay on the PoP that issued the cursor; `blocks` = cursor from the other PoP, replayed by block numbers (deduplicate by `event_id`).'
          replayed: {type: integer, description: Events replayed from the store before live delivery.}
          buffer_complete:
            type: boolean
            description: false when the replay stopped early — read the rest through `GET /v1/wallet/watch/{id}/events?cursor=`.
          cursor: {type: string, description: 'Cursor of the last replayed event (or of the starting position); empty when there is none.'}
      examples:
        - payload: {type: resume, subscription: '2f6a1c0e-9d3b-4c1e-8f0a-7b2d5e6c9a11', after_seq: 0, mode: seq, replayed: 17, buffer_complete: true, cursor: 'c1.eyJwIjoiZXUtb3ZoLTEiLCJzIjoxODIzNCwiYiI6eyJldGgiOjI2MDc5MTc1fSwiayI6IjAyZTVjNWU1MDM4OCJ9'}
    cursorExpired:
      name: cursor_expired
      title: Cursor expired control frame
      summary: The cursor is older than this PoP retains; followed by close code 4410.
      payload:
        type: object
        required: [type, error, subscription, recoverable, oldest_cursor, mode]
        properties:
          type: {type: string, const: cursor_expired}
          error: {type: string, const: cursor_expired}
          subscription: {type: string}
          recoverable: {type: boolean, const: false, description: Events between the cursor and `oldest_cursor` cannot be recovered.}
          oldest_cursor: {type: string, description: Cursor of the oldest retained position; reconnect with it.}
          mode: {type: string, enum: [seq, blocks]}
      examples:
        - payload: {type: cursor_expired, error: cursor_expired, subscription: '2f6a1c0e-9d3b-4c1e-8f0a-7b2d5e6c9a11', recoverable: false, oldest_cursor: 'c1.…', mode: seq}
    walletActivity:
      name: wallet_activity
      title: Wallet activity event
      summary: One transaction in which a watched address takes part.
      payload: {$ref: '#/components/schemas/WalletActivityEvent'}
      examples:
        - name: swap_to_eth
          summary: Real event (Ethereum block 26079176) — token out in a log, ETH back inside the call tree.
          payload:
            type: wallet_activity
            event_id: wa_fee93628d4ac385fdd95f122e55d4654
            seq: 18234
            cursor: 'c1.eyJwIjoiZXUtb3ZoLTEiLCJzIjoxODIzNCwiYiI6eyJldGgiOjI2MDc5MTc1fSwiayI6IjAyZTVjNWU1MDM4OCJ9'
            chain: eth
            chain_id: 1
            block: {number: 26079176, hash: '0x0d5918c5004fd08df5d230156bca545d57967c2956336b153dc991b38a7088ef', timestamp: '2026-09-28T23:15:47Z'}
            tx: {hash: '0x26c296759b03833624a52a933a03733b9f0dc574748dd7c49f66408d5abdceb9', index: 0, from: '0xf13176ece4ed8d9aa4c335cd4f247458d4863fe4', to: '0x7f98c4510ce9434c3ab82be48a4a9899762ed5e4', value: '0', nonce: 17446, type: 2, method_id: '0x39ecce49', gas_used: '316502', effective_gas_price: '32437439794', fee: '10266514569680588'}
            status: success
            error: null
            confirmation: {depth: 1, reorg_window: 12, state: unsafe}
            address: '0xf13176ece4ed8d9aa4c335cd4f247458d4863fe4'
            matched_by: [tx_from, native_internal_in, erc20_out, log_topic]
            movements:
              - {kind: erc20, direction: out, token: '0x8cc7587f649613ed69c23b9192236b5bf998bee4', from: '0xf13176ece4ed8d9aa4c335cd4f247458d4863fe4', to: '0x000000000004444c5dc75cb358380d2e3de08a90', amount: '10885706924859198261659815', log_index: 6}
              - {kind: native_internal, direction: in, token: native, from: '0x7f98c4510ce9434c3ab82be48a4a9899762ed5e4', to: '0xf13176ece4ed8d9aa4c335cd4f247458d4863fe4', amount: '55865764517809644', trace_path: '11'}
            coverage: {logs: full, internal_native: matched_tx_only}
    event:
      name: event
      title: Wallet-watch event
      summary: A detected event of the other types (with enriched participants), or a system event (`reorg_revert`, `gap`).
      payload: {$ref: '#/components/schemas/Event'}
      examples:
        - name: bridge_in
          payload:
            seq: 128417
            event_id: 'ev_…'
            cursor: 'c1.…'
            chain: base
            block: 51314763
            block_hash: '0x…'
            tx: '0x…'
            log_index: 210
            ts: '2026-09-14T22:41:03Z'
            type: bridge_in
            bridge: {protocol: across, direction: in, src_chain: eth, dest_chain: base, match_key: 'across:1846213', contract: '0x09aea4b2242abc8bb4bb78d537a67a245a7bec64'}
            amount: {token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', raw: '1500000000', value: '1500', usd: 1500}
            from: {address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', profile: {address: '0xd8da…', names: [{source: ens, name: vitalik.eth, chain: eth, primary: true, verified: true, normalized: true}]}}
            to: {address: '0x1111…'}
            recipient: {address: '0x1111…', differs_from_sender: true}
            enrich: done
        - name: reorg_revert
          payload: {seq: 18240, event_id: 'ev_…', cursor: 'c1.…', chain: eth, block: 26079176, block_hash: '', tx: '', ts: '2026-09-30T10:00:12Z', type: reorg_revert, reverted: {from_block: 26079176, to_block: 26079176}, removed_event_ids: [wa_fee93628d4ac385fdd95f122e55d4654], removed_seqs: [18234]}
        - name: gap
          payload: {seq: 18302, event_id: 'ev_…', cursor: 'c1.…', chain: bsc, block: 124610180, block_hash: '', tx: '', ts: '2026-09-30T10:00:00Z', type: gap, from_block: 124609901, to_block: 124610180, reason: scanner_lag, recoverable: false}
  schemas:
    Participant:
      type: object
      required: [address]
      properties:
        address: {type: string}
        profile: {type: object, description: Truncated `WalletProfile` (see wallet.yaml) when the subscription's `enrich` lists fields; absent otherwise.}
        differs_from_sender: {type: boolean, description: Bridge recipient ≠ sender (only on `recipient`).}
    Amount:
      type: object
      required: [token, raw]
      properties:
        token: {type: string, description: '`native` or ERC-20 contract address.'}
        symbol: {type: string}
        raw: {type: string, description: Minimal units, decimal string.}
        value: {type: string, description: Token units when decimals are known.}
        usd: {type: number}
        decimals: {type: integer}
    Bridge:
      type: object
      required: [protocol, direction, contract]
      properties:
        protocol: {type: string, enum: [across, stargate, debridge, relay, base_native, polygon_pos, robinhood_native]}
        direction: {type: string, enum: [in, out]}
        src_chain: {type: string}
        dest_chain: {type: string}
        match_key: {type: string, description: Shared id for pairing `bridge_out` with the matching `bridge_in` (depositId / orderId / guid) where the protocol provides one.}
        contract: {type: string}
    Event:
      type: object
      required: [seq, chain, block, type, ts]
      properties:
        seq: {type: integer, description: Position on the delivering PoP (PoP-local).}
        event_id: {type: string, description: 'Stable id (`ev_` + 32 hex): same on every PoP and redelivery; changes when a reorg moves the event to another block.'}
        cursor: {type: string, description: 'Opaque resume position (`c1.…`); reconnect with `?cursor=`.'}
        chain: {type: string, enum: [eth, base, polygon, bsc, robinhood]}
        block: {type: integer}
        block_hash: {type: string}
        tx: {type: string}
        log_index: {type: integer}
        ts: {type: string, format: date-time}
        type: {type: string, enum: [contract_deploy, smart_wallet_deploy, bridge_in, bridge_out, fresh_funding, delegation_set, reorg_revert, gap]}
        bridge: {$ref: '#/components/schemas/Bridge'}
        amount: {$ref: '#/components/schemas/Amount'}
        contract: {type: string, description: Deployed contract / smart wallet address.}
        factory: {type: string}
        kind: {type: string, description: '`erc4337` | `safe` | `coinbase` for smart wallets; `eip7702` for delegations.'}
        delegate: {type: string}
        from: {$ref: '#/components/schemas/Participant'}
        to: {$ref: '#/components/schemas/Participant'}
        recipient: {$ref: '#/components/schemas/Participant'}
        enrich: {type: string, enum: [done, skipped], description: '`skipped` = enrichment budget exhausted; profile-based filters were not applied.'}
        reverted:
          type: object
          properties:
            from_block: {type: integer}
            to_block: {type: integer}
        removed_event_ids:
          type: array
          items: {type: string}
          description: '`reorg_revert` only: ids of events from the reverted blocks that this subscription received (possibly empty). Drop them.'
        removed_seqs:
          type: array
          items: {type: integer}
          description: '`reorg_revert` only: their seqs on the delivering PoP.'
        from_block: {type: integer, description: '`gap` only: first block of the range.'}
        to_block: {type: integer, description: '`gap` only: last block of the range.'}
        reason:
          type: string
          enum: [scanner_lag, rate_limited]
          description: '`gap` only. `scanner_lag`: the scanner skipped the range (sent to every subscription on the network). `rate_limited`: this subscription exceeded its `wallet_activity` events/s cap and part of its events in the range were dropped (sent to that subscription only).'
        recoverable: {type: boolean, const: false, description: '`gap` only: the events will never be delivered.'}
    Movement:
      type: object
      required: [kind, direction, token, from, to, amount]
      properties:
        kind: {type: string, enum: [native, native_internal, erc20, erc721, erc1155]}
        direction: {type: string, enum: [in, out, self]}
        token: {type: string, description: '`native` or the token contract address.'}
        from: {type: string}
        to: {type: string}
        amount: {type: string, description: 'Exact amount in the smallest unit, decimal string (ERC-721: "1").'}
        token_id: {type: string}
        log_index: {type: integer}
        trace_path: {type: string, description: '`native_internal` only: call-tree position ("0" = first call of the root).'}
    WalletActivityEvent:
      type: object
      required: [type, event_id, seq, chain, chain_id, block, tx, status, error, confirmation, address, matched_by, movements, coverage]
      description: Same schema as `WalletActivityEvent` in `product-rest/wallet.yaml`.
      properties:
        type: {type: string, const: wallet_activity}
        event_id: {type: string, description: '`wa_` + first 32 hex of sha256("<chain_id>|<block_hash>|<tx_hash>|<address>").'}
        seq: {type: integer}
        cursor: {type: string}
        chain: {type: string, enum: [eth, base, bsc, robinhood, polygon]}
        chain_id: {type: integer, enum: [1, 8453, 56, 4663, 137]}
        block:
          type: object
          properties:
            number: {type: integer}
            hash: {type: string}
            timestamp: {type: string, format: date-time}
        tx:
          type: object
          properties:
            hash: {type: string}
            index: {type: integer}
            from: {type: string}
            to: {type: [string, 'null']}
            created_contract: {type: string}
            value: {type: string}
            nonce: {type: integer}
            type: {type: integer}
            method_id: {type: [string, 'null']}
            gas_used: {type: string}
            effective_gas_price: {type: string}
            fee: {type: string}
            l1_fee: {type: string}
        status: {type: string, enum: [success, reverted, unknown]}
        error:
          type: [object, 'null']
          properties:
            message: {type: [string, 'null']}
            source: {type: string, enum: [trace, unavailable]}
            data: {type: string}
        confirmation:
          type: object
          properties:
            depth: {type: integer}
            reorg_window: {type: integer}
            state: {type: string, enum: [unsafe, safe, finalized]}
        address: {type: string}
        matched_by:
          type: array
          items:
            type: string
            enum: [tx_from, tx_to, contract_created, native_in, native_out, native_internal_in, native_internal_out, erc20_in, erc20_out, erc721_in, erc721_out, erc1155_in, erc1155_out, approval, weth_deposit, weth_withdrawal, log_topic, log_data, bridge_out, bridge_in_recipient, authorization_7702]
        movements:
          type: array
          items: {$ref: '#/components/schemas/Movement'}
        bridge:
          type: object
          properties:
            protocol: {type: string}
            direction: {type: string, enum: [in, out]}
            src_chain: {type: string}
            dest_chain: {type: string}
            recipient: {type: string}
            recipient_differs: {type: boolean}
            match_key: {type: string, description: Only when the protocol emits an id; links are never inferred.}
            link: {type: string, enum: [protocol_id, none]}
        coverage:
          type: object
          properties:
            logs: {type: string, enum: [full, unavailable]}
            internal_native: {type: string, enum: [traced, matched_tx_only, matched_tx_skipped, unavailable]}
