Farbeam
Docs / SDK guides

Recipe: wallet watch

This recipe watches one address. There are two tools, and each sees different things.

ToolSeesMissesCost
Polling Wallet activityCurated-token transfers of any size on one EVM chain, or all signatures on SolanaOther tokens and native transfers on EVM3 units per poll per chain
A rule on the addressNew stored rows of at least $100,000, at most one per cooldownEverything below that, other tokens, most of SolanaNo units

Use a rule for large moves on Ethereum, Base and BNB Chain. Use polling for the rest. For Solana, use polling, because rules see one block in about 148 and a partial read of 7 exchange wallets.

#Poll for activity

This poll reads Ethereum every 60 seconds, which matches the cache time. At 3 units per poll, one chain costs 3 of your units per minute.

curl -s "https://farbeamlabs.com/api/v1/wallets/0x28C6c06298d514Db089934071355E5743bf21d60/activity?chain=ethereum" \
  | jq '.data | {blocks_scanned, from_block, to_block, items: (.items | length)}'
// watch.mjs: run with node watch.mjs
const ADDRESS = "0x28C6c06298d514Db089934071355E5743bf21d60";
const CHAIN = "ethereum";
const headers = process.env.FARBEAM_KEY ? { Authorization: `Bearer ${process.env.FARBEAM_KEY}` } : {};
const seen = new Set();
let first = true;

async function tick() {
  const res = await fetch(`https://farbeamlabs.com/api/v1/wallets/${ADDRESS}/activity?chain=${CHAIN}`, { headers });
  const { data, error } = await res.json();
  if (error) {
    console.error(error.code, error.message);
    return;
  }
  for (const item of data.items.slice().reverse()) {
    const id = `${item.tx}:${item.token_address}:${item.from}:${item.to}:${item.amount}`;
    if (seen.has(id)) continue;
    seen.add(id);
    if (!first) console.log(item.direction, item.amount, item.token, item.from_label ?? item.from, "->", item.to_label ?? item.to, item.tx);
  }
  first = false;
  console.log(`checked blocks ${data.from_block} to ${data.to_block}, ${data.blocks_scanned} blocks`);
}

await tick();
setInterval(() => tick().catch((err) => console.error(err.message)), 60000);
# watch.py: run with python3 watch.py
import os, time
import requests

ADDRESS = "0x28C6c06298d514Db089934071355E5743bf21d60"
CHAIN = "ethereum"
key = os.environ.get("FARBEAM_KEY")
headers = {"Authorization": f"Bearer {key}"} if key else {}
seen, first = set(), True

while True:
    try:
        body = requests.get(
            f"https://farbeamlabs.com/api/v1/wallets/{ADDRESS}/activity",
            params={"chain": CHAIN},
            headers=headers,
            timeout=30,
        ).json()
        if "error" in body:
            print(body["error"]["code"], body["error"]["message"])
        else:
            data = body["data"]
            for item in reversed(data["items"]):
                marker = (item["tx"], item["token_address"], item["from"], item["to"], item["amount"])
                if marker in seen:
                    continue
                seen.add(marker)
                if not first:
                    print(item["direction"], item["amount"], item["token"], item["tx"])
            first = False
            print("checked", data["from_block"], data["to_block"], data["blocks_scanned"])
    except requests.RequestException as err:
        print("error", err)
    time.sleep(60)

The first pass stores what exists and prints nothing, so only new items print after that.

#What to watch for in the response

  • blocks_scanned can drop from 5,000 to 1,000, 200 or 40 if the RPC refuses the range. A range that is short and a list that is full (40 items, the cap) can hide items between polls on a busy wallet.
  • The list holds curated tokens only. See the scope field.
  • Native ETH is not in this list. Compare native.amount from Wallet overview between polls if you need to see it change. That costs 3 units and covers all six EVM chains.

#Solana

The same path returns the last 25 signatures. Use each new one with Transaction lookup at 2 units.

curl
curl -s "https://farbeamlabs.com/api/v1/wallets/5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9/activity" | jq '.data.items[:3]'

A wallet with more than 25 signatures between polls will lose the extras. For a busy address, shorten the interval and watch your units.

#Add a rule for large moves

This rule sends a webhook when a stored row touches the address. Comparison ignores case, so a checksummed address works.

Beamline
from == "0x28C6c06298d514Db089934071355E5743bf21d60" or to == "0x28C6c06298d514Db089934071355E5743bf21d60"

Save it through the console or Create a rule. The default cooldown of 300 s drops other matches for 5 minutes, so set cooldown_seconds to 60 for a busy address. A list of addresses also works, up to the limit of 1,000 characters:

Beamline
from in ["0x28C6c06298d514Db089934071355E5743bf21d60", "0x21a31Ee1afC51d94C2eFcCAa2092aD1028285549"] or to in ["0x28C6c06298d514Db089934071355E5743bf21d60", "0x21a31Ee1afC51d94C2eFcCAa2092aD1028285549"]

#Limits to state in your own product

  • The watch is not complete. A silent watch does not mean a silent wallet.
  • Polling stops when your process stops. Webhooks do not replay. Rows of at least $100,000 can be recovered for 7 days from List large transfers with the address filter.
  • Wallet clustering is Planned, and it is not built. Watching one address tells you nothing about other addresses the same owner may control.

Use this for addresses you have a reason to monitor, such as your own treasury or public entities. Do not use it to track private individuals. See acceptable use.