Command-line interface for the Formo API. Manage wallet profiles, alerts, dashboards, charts, contracts, segments, and run analytics SQL — directly from your terminal or via AI agents.
npm install -g @formo/cli
# or use without installing:
npx @formo/cliThis repository also ships the formo-analytics skill for Claude Code, Codex, and other Agent Skills-compatible tools. It teaches agents to use Formo's MCP server, CLI, and REST API for project-scoped product and onchain analytics.
Install it in the current project:
npx skills add https://github.com/getformo/cli/tree/main/skills/formo-analyticsOr install it globally for Claude Code and Codex:
npx skills add getformo/cli --skill formo-analytics --global \
--agent claude-code --agent codexSave your API key locally:
formo login <apiKey>Or set the FORMO_API_KEY environment variable — it takes precedence over the saved config:
export FORMO_API_KEY=formo_abc123Get your API key from Settings → API in the Formo dashboard.
For local development or proxying, override API hosts with:
export FORMO_API_BASE_URL=http://localhost:3001
export FORMO_EVENTS_BASE_URL=http://localhost:3002Save your API key to ~/.config/formo/config.json. Validates the key against the API and stores the workspace context.
formo login formo_abc123Remove the saved API key and clear authentication state.
formo logoutShow current authentication state, workspace, and project ID.
formo statusWallet profile commands.
Fetch a single wallet profile by address or ENS name.
| Option | Description |
|---|---|
--expand |
Comma-separated fields: apps, chains, tokens, labels |
--timestamp |
ISO-8601 timestamp; return the closest stored wallet-enrichment snapshot |
formo profiles get 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
formo profiles get vitalik.eth --expand labels,chains
formo profiles get vitalik.eth --timestamp 2025-06-21T10:03:00ZSearch wallet profiles with filters, sorting, and pagination. Returns a PaginatedResponse<Profile>.
| Option | Description |
|---|---|
--address |
Filter by wallet address |
--search |
Free-text search across address and identity fields |
--timestamp |
ISO-8601 timestamp; requires --address and returns the closest stored wallet-enrichment snapshot |
--page |
Page number (1-indexed, default 1) |
--size |
Page size (default 100, max 1000) |
--order-by |
last_onchain, first_onchain, net_worth_usd, updated_at, tx_count, first_seen, last_seen, num_sessions, revenue, volume, points |
--order-dir |
asc or desc |
--expand |
Comma-separated fields to expand |
--filters |
JSON array of canonical {field,op,value} filter objects (see below) |
--logic |
Combine filters with and (default) or or |
formo profiles search --size 10
formo profiles search --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --timestamp 2025-06-21T10:03:00Z
formo profiles search --order-by net_worth_usd --order-dir desc --size 5
formo profiles search --page 2 --size 20
formo profiles search --filters '[{"field":"users.net_worth_usd","op":"gt","value":10000}]' --size 20
formo profiles search --filters '[{"field":"users.net_worth_usd","op":"gt","value":10000},{"field":"users.volume","op":"gt","value":1000}]' --logic or --size 20
formo profiles search --filters '[{"field":"chains.balance","op":"gt","value":1000,"chain_id":"1"}]' --size 20With --timestamp, wallet-enrichment fields come from the stored snapshot closest to that instant. Exact ties select the later snapshot. Expanded chains, apps, and tokens come from the selected profiling batch, while project engagement fields, identity overrides, and labels remain current. profiles search --timestamp requires --address.
Both profiles get and profiles search accept optional flags to override the lifecycle stage thresholds used when computing lifecycle:
| Option | Description |
|---|---|
--new-window-days |
Override lifecycle new-user window in days |
--churn-window-days |
Override lifecycle churn window in days |
--power-user-min-active-days |
Override lifecycle power-user minimum active days |
--power-user-window-days |
Override lifecycle power-user window in days |
--resurrected-gap-days |
Override lifecycle resurrected gap in days |
--at-risk-min-days-inactive |
Override lifecycle at-risk minimum inactive days |
--at-risk-prior-active-days-threshold |
Override lifecycle at-risk prior active days threshold |
Merge-update identity properties on a wallet profile.
| Option | Description |
|---|---|
--properties |
JSON object of properties to merge |
Allowed property keys: user_id, display_name, email, farcaster, discord, twitter, telegram, instagram, website, github, linkedin, facebook, tiktok, youtube, reddit, avatar, description, location, ens, lens, basenames, linea. Unknown keys are rejected server-side.
formo profiles update 0xd8dA... --properties '{"display_name":"Vitalik","twitter":"VitalikButerin"}'
formo profiles update vitalik.eth --properties '{"email":"alice@example.com"}'Requires
profiles:writescope.
Batch update first-party properties for up to 100 wallets.
formo profiles properties batch \
--rows '[{"address":"0xd8dA...","display_name":"alice.eth","email":"alice@example.com"}]'Upsert one or more labels on a wallet profile. Provide either a single label via --tag-id or a batch via --labels.
| Option | Description |
|---|---|
--tag-id |
Label identifier (e.g. vip, airdrop_eligible) |
--value |
Optional label value (e.g. tier name, country code) |
--chain-id |
Optional chain identifier the label applies to |
--timestamp |
Optional historical ISO-8601 timestamp |
--is-deleted |
Backfill a historical label removal tombstone |
--labels |
JSON array of UserLabelInput objects for batch upsert |
formo profiles labels create 0xd8dA... --tag-id vip
formo profiles labels create 0xd8dA... --tag-id tier --value gold --chain-id 1
formo profiles labels create 0xd8dA... --labels '[{"tag_id":"vip"},{"tag_id":"airdrop_eligible","chain_id":"1"}]'
formo profiles labels create 0xd8dA... --tag-id tier --timestamp 2024-03-15T00:00:00.000Z --is-deleted
formo profiles labels batch --labels '[{"address":"0xd8dA...","tag_id":"vip","value":"tier-1"}]'Delete a label from a wallet profile.
| Option | Description |
|---|---|
--tag-id |
Label identifier to delete (required) |
--chain-id |
Optional chain identifier to scope the deletion |
formo profiles labels delete 0xd8dA... --tag-id vip
formo profiles labels delete 0xd8dA... --tag-id tier --chain-id 1Requires
profiles:writescope.
Project alert commands. Requires alerts:read (list/get) or alerts:write (create/update/delete/toggle).
List all alerts for the project.
Get a single alert by ID.
| Option | Description |
|---|---|
--name |
Alert name |
--trigger-type |
Trigger type: event or user |
--trigger-filters |
JSON array of trigger filter objects |
--recipient |
JSON array of recipient objects |
--secret |
Webhook secret |
--slack-property-keys |
JSON array of event/user property keys to include in Slack alerts |
formo alerts create --name "High value tx" --trigger-type event \
--trigger-filters '[{"field":"event","op":"eq","value":"transaction"}]' \
--recipient '[{"type":"email","value":["alerts@myapp.com"]}]'Same options as create. Replaces the alert configuration in full — omitted options are reset to their defaults (e.g. leaving out --trigger-filters clears the existing trigger filters).
Delete an alert.
Toggle an alert between active and inactive.
formo alerts toggle alert_abc123 --status inactiveDashboard board commands. Requires boards:read / boards:write.
List all boards for the project.
Get a single board by ID.
| Option | Description |
|---|---|
--title |
Board title |
--description |
Optional board description |
--is-public |
Make the board publicly viewable |
formo boards create --title "Revenue Metrics" --description "Weekly revenue tracking"| Option | Description |
|---|---|
--title |
New board title |
--description |
New board description |
--is-public |
Update public visibility |
Delete a board.
Chart commands. Charts live inside a board. Requires boards:read / boards:write.
List charts in a board. Returns lightweight summaries by default; pass --results to execute each chart’s query and include full results.
Get a single chart by ID.
List lightweight chart metadata without executing chart queries.
Create a chart from typed flags or a raw JSON body.
formo charts create --board-id brd_123 --title "Daily Active Users" \
--chart-type line \
--query "SELECT toDate(timestamp) AS date, countDistinct(address) AS users FROM events GROUP BY date ORDER BY date" \
--x-axis date --y-axis users
formo charts create --board-id brd_123 --body '{"title":"Recent Events","chart_type":"table","query":"SELECT * FROM events LIMIT 10"}'
formo charts create --board-id brd_123 --title "Post-connect paths" \
--chart-type user_paths \
--settings '{"anchors":[{"type":"event","event":"connect"}],"maxSteps":5,"nodesPerStep":3}'Update a chart. Accepts the same options as create: a raw --body '<json>' and/or typed flags (--title, --chart-type, --query, --description, --x-axis, --y-axis, --group-by, --steps, --settings). --steps is for funnel steps. User Paths use --settings with anchors and optional maxSteps / nodesPerStep; retention settings require an entryFilter key. Typed flags override matching --body keys.
formo charts update chart_abc123 --board-id brd_123 --title "Renamed chart"Execute a saved chart that uses {{date_from}} / {{date_to}} variables.
Move a chart to another board, duplicate a chart, or reorder charts in a board.
Delete a chart.
Smart contract commands. Requires contracts:read / contracts:write.
List all tracked contracts. Returns { data: Contract[], deploy: { last_deployed_at, diff }, total, page, size, has_more }.
Get a single tracked contract.
| Option | Description |
|---|---|
--address |
Contract address (0x…) |
--chain |
Chain ID (e.g. 1, 137) |
--name |
Human-readable contract name |
--abi |
Contract ABI as a JSON string; sent stringified to the API |
--events |
JSON array of ABI event objects to monitor |
--start-block |
Optional start block |
--include-in-pipeline |
Include this contract in the Goldsky events pipeline (true by default in the API) |
formo contracts create --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 --chain 1 \
--name "UNI Token" --abi '[{"type":"event","name":"Transfer","inputs":[]}]' \
--events '[{"type":"event","name":"Transfer","inputs":[]}]'| Option | Description |
|---|---|
--name |
Updated contract name |
--abi |
Updated ABI |
--events |
Updated JSON array of ABI event objects |
--start-block |
Optional start block |
--include-in-pipeline |
Include or exclude this contract from the Goldsky events pipeline |
Remove a tracked contract.
User segment commands. Requires segments:read / segments:write.
List all user segments.
| Option | Description |
|---|---|
--title |
Segment title |
--filters |
JSON array of canonical {field,op,value} filter objects. Array string members cannot contain ` |
Delete a user segment.
Run a SQL query against your Formo analytics data. Returns { data, total, limit, offset, has_more }.
formo query run "SELECT count(*) FROM events"
formo query run "SELECT address, net_worth_usd FROM wallet_profiles ORDER BY net_worth_usd DESC LIMIT 10"Requires
query:readscope.
Pre-built analytics pipes — the same data that powers the Formo dashboard — without writing SQL. Each pipe is a subcommand: formo analytics <pipe>.
Pipes: kpis, event_timeseries, funnel, flow, frequency, lifecycle, retention, revenue_overview, revenue_by_metric, revenue_timeseries, volume_by_metric, top_chains, top_events, top_locations, top_pages, top_sources, top_wallets
| Option | Description |
|---|---|
--date-from |
Inclusive start date YYYY-MM-DD (default: 7 days before --date-to) |
--date-to |
Inclusive end date YYYY-MM-DD (default: today) |
--filters |
JSON array of [{field,op,value}]. For in/nin, array values are preferred; pipe-delimited strings are also accepted. Array string members cannot contain ` |
--params |
JSON object of pipe-specific params merged into the query (e.g. {"limit":10,"group_by":"device"}) |
formo analytics kpis
formo analytics kpis --date-from 2026-04-01 --date-to 2026-04-30 --params '{"group_by":"device"}'
formo analytics funnel --date-from 2026-04-01 --date-to 2026-04-30 --params '{"steps":[{"type":"event","event":"page","name":"page::0","filters":[]},{"type":"track","event":"connect","name":"connect::1","filters":[]}],"window_seconds":86400}'
formo analytics top_wallets --date-from 2026-04-01 --date-to 2026-04-30 --params '{"limit":10}'
formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"}]'Requires
query:readscope. Runformo analytics <pipe> --helpfor the pipe-specific params accepted via--params.
On kpis, top_*, revenue_* and volume_by_metric, --params '{"page_scope":"session"}' widens a page filter from page-scoped metrics (the default) to the legacy session scope.
All user-attribute, profile, social, lifecycle and resource predicates go in the single --filters array, using the canonical envelope with named qualifiers (chain_id, app_id, token_address, scope, tag_id). The retired per-family params — socials, chain_filters, app_filters, token_filters, label_filters, profile_filters, lifecycle_filter — are rejected with a 400 if passed through --params.
Bulk-import wallet addresses into the project via the main Formo API, authenticated with your workspace API key.
| Option | Description |
|---|---|
--addresses |
JSON array of wallet address strings |
--rows |
JSON array of {address,properties?} objects |
formo import wallets --addresses '["0xabc...","0xdef..."]'
formo import wallets --rows '[{"address":"0xabc...","properties":{"display_name":"Alice"}}]'Requires
profiles:writescope. Only available on Scale and Enterprise plans.
Send raw analytics events to events.formo.so. This command uses a project SDK write key, not the workspace API key — pass it via --write-key or the FORMO_WRITE_KEY environment variable.
| Option | Description |
|---|---|
--event |
Single event as a JSON object; wrapped in an array before sending |
--events |
JSON array of event objects to send as a batch |
--write-key |
Project SDK write key (defaults to FORMO_WRITE_KEY) |
export FORMO_WRITE_KEY=formo_write_key_xxx
formo events ingest --event '{"type":"track","channel":"cli","version":"1","anonymous_id":"anon_123","event":"CLI Test","context":{},"properties":{},"original_timestamp":"2026-04-27T23:05:38.000Z","sent_at":"2026-04-27T23:05:42.000Z","message_id":"cli-test-1"}'
formo events ingest --events '[{"type":"track","event":"First"},{"type":"track","event":"Second"}]'profiles search --filters accepts a JSON array of canonical filter objects:
[
{ "field": "users.net_worth_usd", "op": "gt", "value": 10000 },
{ "field": "chains.balance", "op": "gte", "value": 1000, "chain_id": "1" }
]The
fieldmust be a canonical path. A bare name likenet_worth_usdis silently ignored by the API (no error, no filtering — the search returns everything). Resource identity goes in the named qualifiers below, never in the field path — identifier-in-path fields such aschains.1.balanceare rejected with a400.
| Field | Type | Description |
|---|---|---|
field |
string |
users.{attribute} or one of the four resource paths (see below) |
op |
string |
eq, neq, gt, gte, lt, lte, in, nin, contains, startsWith, endsWith, notEmpty / isEmpty (value-less existence checks). Support is per field — see operator support. Long-form spellings (equals, greater, includes, …) are retired — the API rejects them with a 400 naming the token |
value |
any |
Value to compare against; must be a number on the .balance fields |
chain_id |
string |
(optional on any resource filter) restrict to one chain; omit to match any |
app_id |
string |
(required by apps.balance, and by tokens.balance with scope: protocol) e.g. aave-v3 |
token_address |
string |
(required by tokens.balance) |
tag_id |
string |
(required by labels.value) e.g. coinbase.verified_account |
scope |
string |
(required by tokens.balance) any or protocol |
| Field | Required qualifiers | Example |
|---|---|---|
users.{attribute} |
none | {"field":"users.net_worth_usd","op":"gt","value":10000} |
chains.balance |
none (chain_id optional) |
{"field":"chains.balance","op":"gte","value":1000,"chain_id":"1"} |
apps.balance |
app_id |
{"field":"apps.balance","op":"gt","value":500,"app_id":"uniswap-v3"} |
tokens.balance |
token_address, scope |
{"field":"tokens.balance","op":"gt","value":0,"token_address":"0xA0b8…48","scope":"any"} |
labels.value |
tag_id |
{"field":"labels.value","op":"eq","value":"true","tag_id":"coinbase.verified_account"} |
User attributes for users.{attribute}: net_worth_usd, volume, revenue,
points, device, location, lifecycle, ens, farcaster, and the other
social handles.
The canonical vocabulary is shared, but each field implements a subset. The API
rejects an unsupported pairing with a 400:
| Field class | Supported operators |
|---|---|
chains.balance, apps.balance, tokens.balance |
eq, neq, gt, gte, lt, lte (value must be a JSON number) |
labels.value |
comparison operators plus contains (case-insensitive) |
Numeric profile metrics (users.net_worth_usd, users.volume, users.revenue, users.points) |
comparison operators |
Routable string attributes (users.device, users.os, users.referrer, users.utm_*, users.click_id, and the first_*/last_* attribution variants) |
full vocabulary; contains/startsWith/endsWith match case-sensitively |
Social fields (users.twitter, users.email, users.farcaster, …) |
contains (case-insensitive) and notEmpty; startsWith/endsWith/isEmpty are rejected |
users.paid_source (and first_/last_ variants) |
eq, neq, in, nin, notEmpty, isEmpty — it is a fixed ad-network enum |
users.lifecycle |
eq (one stage) and in (a list of stages) |
Combine multiple filters with --logic and (default) or --logic or.
Every command supports the standard incur output flags:
| Flag | Description |
|---|---|
--format <toon|json|yaml|md|jsonl> |
Output format (default: toon) |
--json |
Shorthand for --format json |
--verbose |
Include the full envelope (ok, data, meta) |
--filter-output <keys> |
Filter output by key paths (e.g. data,meta.duration) |
Every list endpoint returns a PaginatedResponse<T> envelope: { data: [...], total, page, size, has_more }. Every error follows: { error: { code, message, doc_url, param?, details? } } — branch on error.code, not message.
# Install dependencies
pnpm install
# Run in development mode
pnpm dev
# Build TypeScript
pnpm build
# Lint
pnpm lint
# Run tests (requires TEST_TOKEN in .env)
pnpm test