Wallet watch — event subscriptions
Subscribe to what wallets do across the EVM networks Triport monitors: contract and smart-wallet deployments, bridge deposits and fills, funding of fresh wallets, EIP-7702 delegations — every participant enriched with the cross-chain identity profile and matched against your filter, delivered by webhook, WebSocket or a REST cursor. For every transaction of the addresses on your watchlist, use the wallet_activity event type.
https://triport.io/v1/wallet/watch- Network
- Ethereum, Base, Polygon, BNB Smart Chain, Robinhood Chain (Tron is not monitored)
- Scope
- — (tier-gated, see below)
- Auth
- x-token: $TRIPORT_API_KEY (or Authorization: Bearer) — the key must belong to an account (dashboard key), not a static token
- Rate limit
- Business — 30 rps (wallet_identity), 5 subscriptions × 5 networks, watchlist ≤ 10 000, webhook 10 rps; Enterprise — 150 rps, 50 subscriptions × 30 networks, watchlist ≤ 100 000
A subscription names the networks and event types you care about, an
optional filter evaluated against each
participant's profile, the profile groups to attach (enrich), an optional
watchlist of addresses, a USD floor for bridge amounts, and a delivery
channel. Triport's scanner follows every monitored network block by block
(full transactions plus receipts, with a reorg window), detects events,
stores them with a monotonic per-PoP sequence number, and matches them
against all enabled subscriptions.
Event types:
| Type | Trigger | Participants |
|---|---|---|
contract_deploy | to == null transaction that left contractAddress in the receipt | from (deployer), contract |
smart_wallet_deploy | AccountDeployed from EntryPoint v0.6/0.7/0.8, ProxyCreation from Safe ProxyFactory 1.3.0/1.4.1, Coinbase Smart Wallet factory calls | from, to (new wallet), factory, kind |
bridge_out | Across FundsDeposited, Stargate v2 OFTSent, deBridge CreatedOrder, Polygon PoS LockedERC20, Base/Robinhood L1 deposit initiations, transfers to a Relay solver | from, to (bridge), recipient (+ differs_from_sender), bridge.match_key |
bridge_in | Across FilledRelay, Stargate OFTReceived, deBridge FulfilledOrder, Base L2 DepositFinalized, Robinhood L2 gateway, Polygon ExitedERC20, transfers from a solver | from, to, recipient, bridge.match_key |
fresh_funding | Native value or ERC-20 Transfer to an address with nonce 0, no code and zero balance before the block | from, to (fresh wallet), amount |
delegation_set | EIP-7702 type-4 transaction | from, delegate |
wallet_activity | Any transaction in which a watchlist address takes part (sender/target, native and token movements, approvals, the address in a log, bridges). Own payload shape; see Wallet activity | address (the watched address), movements[] |
reorg_revert | Chain reorganisation removed previously delivered blocks | reverted.from_block…to_block, removed_event_ids of this subscription (always delivered) |
gap | Events of a block range will never be delivered (reason: scanner_lag or rate_limited, recoverable: false) | from_block…to_block (always delivered) |
Enrichment runs through the same profile cache as the identity endpoint
and is capped per network; when the cap is hit the event is delivered with
enrich: "skipped" to subscriptions without profile predicates and dropped
for subscriptions whose filter needs the profile.
Parameters
Request body (POST, PATCH partial)
namestringrequiredchainsstring[]requiredeth,base,polygon,bsc,robinhood monitored on this PoP. Max 5 (Business), 30 (Enterprise).eventsstring[]requiredreorg_revert and gap are always delivered).filterobjectoptionalfresh_wallet is allowed here. Evaluated per participant — the event matches if any participant passes.enrichstring[]optionalnames,balances,activity,labels,deployer.watchliststring[]optionalwallet_activity. Max 10 000 (Business); change it with POST /v1/wallet/watch/{id}/watchlist.min_amount_usdnumberoptionaldeliveryobjectrequired{"type":"webhook","url":"https://…","secret":"…"}, {"type":"ws"} or {"type":"pull"}.enabledbooleanoptionaltrue; false pauses matching.min_confirmationsintegeroptionalResponse
An event as delivered (webhook body, WS frame, or events[] item of the cursor):
{
"seq": 128417,
"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
}
],
"chains": {
"eth": {
"balance": "1.2345",
"nonce": 812,
"is_contract": false
}
},
"unresolved": [],
"meta": {
"cached": true,
"generated_at": "…",
"pop": "eu",
"cost": {}
}
}
},
"to": {
"address": "0x1111…"
},
"recipient": {
"address": "0x1111…",
"differs_from_sender": true
},
"enrich": "done"
}iduuid?id= of the WebSocket and the path segment of the cursor.delivery.secretstring***) in responses.seq (event)integercursor instead (it also works on the other PoP).event_id (event)stringcursor (event)string?cursor=.bridge.match_key (event)stringbridge_out with the bridge_in on the destination network (across:<depositId>, stargate:<guid>, debridge:<orderId>, robinhood_native:<sequence>); absent where the protocol has no shared id.recipient.differs_from_sender (event)booleanenrich (event)stringdone or skipped (enrichment cap reached).Errors
| Code | Meaning | When it happens |
|---|---|---|
400 | invalid_body / invalid_name / invalid_chains / unknown_chain / too_many_chains / invalid_events / unknown_event / invalid_filter / invalid_enrich / invalid_watchlist / too_many_watchlist / invalid_min_amount_usd / invalid_delivery / invalid_after / invalid_limit / watchlist_required / filter_not_supported / invalid_min_confirmations / invalid_cursor / cursor_subscription_mismatch | Validation failed; the message names the field. Webhook URLs must be https://. |
401 | account_required | The API key is a static token without an account; create a key in the dashboard. |
403 | tier_insufficient / subscription_limit | Tier below Business, or the per-tier subscription cap is reached. |
404 | not_found | No such subscription for this account. |
410 | cursor_expired | The cursor is older than the retained window; the body has oldest_cursor and recoverable: false. |
429 | rate_limited | wallet_identity sustained RPS exceeded. |
503 | capacity_exceeded / busy / option_unavailable / events_unavailable | The watched-address cap would be exceeded, another address change is being applied (retry), min_confirmations above 0 is not supported by this deployment yet, or the event store is off on this PoP. |
All error bodies use the shared envelope (error, message, request_id).
See errors.md for the full envelope and per-code fields.
Examples
Verify a webhook signature (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return header?.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Consume the WebSocket with resume (Node)
import WebSocket from "ws";
let last = Number(process.env.LAST_SEQ ?? 0);
function connect() {
const ws = new WebSocket(`wss://triport.io/ws/wallet-watch?id=${SUB_ID}&after_seq=${last}`, {
headers: { Authorization: `Bearer ${process.env.TRIPORT_API_KEY}` },
});
ws.on("message", (raw) => {
const m = JSON.parse(raw);
if (m.type === "resume") { if (!m.buffer_complete) console.warn("gap before", m.after_seq); return; }
last = m.seq;
if (m.type === "reorg_revert") { /* drop state for blocks in m.reverted */ return; }
handle(m);
});
ws.on("close", () => setTimeout(connect, 1000));
}
connect();Poll with the REST cursor (Python)
import os, time, requests
API = f"https://triport.io/v1/wallet/watch/{os.environ['SUB_ID']}/events"
H = {"Authorization": f"Bearer {os.environ['TRIPORT_API_KEY']}"}
after = 0
while True:
page = requests.get(API, params={"after": after, "limit": 200}, headers=H, timeout=30).json()
for ev in page["events"]:
print(ev["seq"], ev["chain"], ev["type"], ev.get("from", {}).get("address"))
after = page["next_after"]
if not page["has_more"]:
time.sleep(5)Fresh wallets funded by a bridge on Robinhood Chain
{
"name": "rh-fresh-from-bridges",
"chains": ["robinhood"],
"events": ["fresh_funding", "bridge_in"],
"filter": {"any": [{"fresh_wallet": true}, {"has_label": ["bridge", "solver"]}]},
"enrich": ["names", "labels"],
"delivery": {"type": "ws"}
}Notes
- Retention: events live 72 hours (or until the 5 GiB per-PoP cap) in
the PoP that observed them; an older cursor gets
410 cursor_expired(WebSocket: close code4410) witholdest_cursor. Subscriptions themselves are shared across PoPs. - Reorgs: the scanner keeps a hash window (12 blocks on Ethereum, 64 on
the L2s and BNB Smart Chain); on a mismatch it deletes the affected events
and delivers
reorg_revertbefore re-scanning the canonical blocks. - Bridge coverage: contracts were confirmed with live logs on both PoPs for Across (all four networks), Stargate v2 (Ethereum, Base), deBridge (Ethereum, Base, Polygon source), Base L2 bridge, Polygon PoS predicate and the Robinhood L1 gateways; pools/gateways without a live log in the probe window (Stargate on Polygon/BSC, deBridge on BSC, Robinhood L2 gateways) are decoded on the same ABI but marked unconfirmed in the bridge ledger.
- USD floors:
min_amount_usdapplies only when the amount is priced — stablecoins are always priced at 1 USD; native coins need a rate, so a floor on unpriced tokens never matches. - Related: wallet activity, profile, batch, filters, streaming overview.