Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Or set the `FORMO_API_KEY` environment variable — it takes precedence over the
export FORMO_API_KEY=formo_abc123
```

Get your API key from `Settings → API Keys` in the [Formo dashboard](https://app.formo.so).
Get your API key from `Settings → API` in the [Formo dashboard](https://app.formo.so).

For local development or proxying, override API hosts with:

Expand Down Expand Up @@ -208,7 +208,7 @@ Get a single alert by ID.

```bash
formo alerts create --name "High value tx" --trigger-type event \
--trigger-filters '[{"name":"event","operator":"equals","value":"transaction"}]' \
--trigger-filters '[{"name":"event","operator":"eq","value":"transaction"}]' \
--recipient '[{"type":"email","value":["alerts@myapp.com"]}]'
```

Expand Down Expand Up @@ -401,15 +401,15 @@ Pre-built analytics pipes — the same data that powers the Formo dashboard —
|---|---|
| `--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}]`. Use `in`/`notIn` with a pipe-delimited value (e.g. `"chrome\|firefox"`) |
| `--filters` | JSON array of `[{field,op,value}]`. Use `in`/`nin` with a pipe-delimited value (e.g. `"chrome\|firefox"`) |
| `--params` | JSON object of pipe-specific params merged into the query (e.g. `{"limit":10,"group_by":"device"}`) |

```bash
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":"equals","value":"US"}]'
formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"}]'
```

> Requires `query:read` scope. Run `formo analytics <pipe> --help` for the pipe-specific params accepted via `--params`.
Expand Down Expand Up @@ -465,7 +465,7 @@ formo events ingest --event '{"type":"track","channel":"cli","version":"1","anon
| Field | Type | Description |
|---|---|---|
| `field` | `string` | Typed path (see prefixes below) |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains` (social fields only), `notEmpty` / `isEmpty` (value-less existence checks). Long-form spellings (`equals`, `greater`, `includes`, …) are deprecated aliases the API still accepts |
| `value` | `any` | Value to compare against |
| `scope` | `string` | _(token filters only)_ `any` or `protocol` |
| `appId` | `string` | _(token filters with `scope: protocol`)_ e.g. `aave-v3` |
Expand Down
8 changes: 4 additions & 4 deletions SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ formo profiles search --order-by tx_count --order-dir desc --page 2 --size 20 --
| Property | Type | Description |
|---|---|---|
| `field` | `string` | **Typed path** — a bare name like `net_worth_usd` is silently ignored by the API |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains` (social fields only), `notEmpty` / `isEmpty` (value-less existence checks). Long-form spellings (`equals`, `greater`, `includes`, …) are deprecated aliases the API still accepts |
| `value` | `any` | Value to compare against |
| `scope` | `string` | _(token filters only)_ `any` or `protocol` |
| `appId` | `string` | _(token filters with `scope: protocol`)_ e.g. `aave-v3` |
Expand Down Expand Up @@ -194,7 +194,7 @@ formo analytics <pipe> [options]
|---|---|
| `--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}]`. Use `in`/`notIn` with a pipe-delimited value (e.g. `"chrome\|firefox"`) |
| `--filters` | JSON array of `[{field,op,value}]`. Use `in`/`nin` with a pipe-delimited value (e.g. `"chrome\|firefox"`) |
| `--params` | JSON object of pipe-specific params merged into the query (e.g. `{"limit":10,"group_by":"device"}`) |

**Examples:**
Expand All @@ -212,7 +212,7 @@ formo analytics funnel --date-from 2026-04-01 --date-to 2026-04-30 --params '{"s
formo analytics top_wallets --date-from 2026-04-01 --date-to 2026-04-30 --params '{"limit":10}'

# Retention filtered to US visitors
formo analytics retention --filters '[{"field":"location","op":"equals","value":"US"}]'
formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"}]'
```

Each pipe accepts pipe-specific params via `--params` (see each command's `--help`): e.g. `funnel` → `steps`, `window_seconds`, `funnel_type`, `breakdown`; `kpis` → `group_by`, `limit`; `top_*` → `limit`, `offset`.
Expand Down Expand Up @@ -266,7 +266,7 @@ formo alerts create --name "High value tx" --trigger-type event

# Create an alert with filters and recipients
formo alerts create --name "Whale alert" --trigger-type event \
--trigger-filters '[{"name":"revenue","operator":"greater_than","value":"100000"}]' \
--trigger-filters '[{"name":"revenue","operator":"gt","value":"100000"}]' \
--recipient '[{"type":"webhook","value":["https://hooks.example.com/formo"]}]'
```

Expand Down
2 changes: 1 addition & 1 deletion src/commands/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const sharedOptions = z.object({
.optional()
.describe(
'JSON array of filter conditions: [{"field","op","value"}]. ' +
'Use op "in"/"notIn" with a pipe-delimited value (e.g. "chrome|firefox").',
'Use op "in"/"nin" with a pipe-delimited value (e.g. "chrome|firefox").',
),
params: z
.string()
Expand Down
4 changes: 3 additions & 1 deletion src/commands/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,9 @@ profiles.command('search', {
'Chains: chains.balance or chains.{chain_id}.balance. ' +
'Apps: apps.{app_id}.balance. Tokens: tokens.{address}.balance ' +
'(optional "scope":"any"|"protocol" + "appId"). Labels: labels.{tag_id}. ' +
'op: eq, neq, gt, gte, lt, lte, in, nin.',
'op: eq, neq, gt, gte, lt, lte, in, nin, contains, notEmpty, isEmpty ' +
'(contains = substring, social fields only; notEmpty/isEmpty = value-less existence checks on string fields). ' +
'Long-form spellings (equals, notEquals, greater, greaterOrEqual, less, lessOrEqual, notIn, includes) are deprecated aliases the API still accepts.',
),
logic: z
.enum(['and', 'or'])
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function loginGuide(): string {
color.boldGreen("How to get your API key:"),
"",
` ${color.white("1.")} Go to ${color.cyan(DASHBOARD_URL)}`,
` ${color.white("2.")} Navigate to ${color.bold("Settings")} → ${color.bold("API Keys")}`,
` ${color.white("2.")} Navigate to ${color.bold("Settings")} → ${color.bold("API")}`,
` ${color.white("3.")} Click ${color.bold('"Create API Key"')} and copy the key`,
` ${color.white("4.")} Run:`,
"",
Expand Down
4 changes: 2 additions & 2 deletions test/commands/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ describe('commands/analytics', function () {

it('re-serializes a valid filters JSON array as a string', function () {
const params = buildAnalyticsParams({
filters: '[{"field":"location","op":"equals","value":"US"}]',
filters: '[{"field":"location","op":"eq","value":"US"}]',
});
expect(params.filters).to.equal(
'[{"field":"location","op":"equals","value":"US"}]',
'[{"field":"location","op":"eq","value":"US"}]',
);
});

Expand Down
11 changes: 9 additions & 2 deletions test/commands/bodyBuilders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ describe('commands / body builders', function () {
const body = buildAlertBody({
name: 'x',
triggerType: 'event',
triggerFilters: '[{"name":"event","operator":"equals","value":"transaction"}]',
triggerFilters: '[{"name":"event","operator":"eq","value":"transaction"}]',
});
expect(body.trigger_filters).to.deep.equal([
{ name: 'event', operator: 'equals', value: 'transaction' },
{ name: 'event', operator: 'eq', value: 'transaction' },
]);
});

Expand Down Expand Up @@ -386,6 +386,13 @@ describe('commands / body builders', function () {
).to.not.throw();
});

it('passes deprecated long-form op aliases through verbatim (server folds them to canonical)', function () {
const conds = parseSearchConditions(
'[{"field":"users.net_worth_usd","op":"greater","value":10000}]',
);
expect((conds[0] as { op: string }).op).to.equal('greater');
});

it('rejects a bare (untyped) field — the silent-failure footgun', function () {
expect(() =>
parseSearchConditions('[{"field":"net_worth_usd","op":"gt","value":10000}]'),
Expand Down