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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ 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 |

```bash
formo profiles get 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
formo profiles get vitalik.eth --expand labels,chains
formo profiles get vitalik.eth --timestamp 2025-06-21T10:03:00Z
```

### `profiles search`
Expand All @@ -105,6 +107,7 @@ Search wallet profiles with filters, sorting, and pagination. Returns a `Paginat
|---|---|
| `--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` |
Expand All @@ -115,13 +118,16 @@ Search wallet profiles with filters, sorting, and pagination. Returns a `Paginat

```bash
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 20
```

With `--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`.

### Lifecycle tuning (advanced)

Both `profiles get` and `profiles search` accept optional flags to override the lifecycle stage thresholds used when computing `lifecycle`:
Expand Down
6 changes: 6 additions & 0 deletions SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Fetch the full profile for a single wallet by address or ENS name.
```bash
formo profiles get <address>
formo profiles get <address> --expand labels,chains,apps,tokens
formo profiles get <address> --timestamp 2025-06-21T10:03:00Z
```

| Argument | Description |
Expand All @@ -60,6 +61,7 @@ formo profiles get <address> --expand labels,chains,apps,tokens
| Option | Description |
|---|---|
| `--expand` | Comma-separated fields to include in full: `apps`, `chains`, `tokens`, `labels` |
| `--timestamp` | ISO-8601 timestamp; return the closest stored wallet-enrichment snapshot |

**Examples:**
```bash
Expand All @@ -84,6 +86,7 @@ formo profiles search [options]
|---|---|---|
| `--address` | `string` | Filter to a specific wallet address |
| `--search` | `string` | Free-text search across address and identity fields |
| `--timestamp` | `string` | ISO-8601 timestamp; requires `--address` and returns the closest stored wallet-enrichment snapshot |
| `--page` | `number` | Page number (1-indexed, default `1`) |
| `--size` | `number` | Page size (default `100`, max `1000`) |
| `--order-by` | see below | Field to sort by |
Expand All @@ -99,6 +102,9 @@ formo profiles search [options]
# First 10 profiles
formo profiles search --size 10

# Closest stored snapshot for one wallet
formo profiles search --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --timestamp 2025-06-21T10:03:00Z

# Top 5 by net worth (descending)
formo profiles search --order-by net_worth_usd --order-dir desc --size 5

Expand Down
3 changes: 3 additions & 0 deletions skills/formo-analytics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Use a direct lookup for one address or ENS name. Use search for cohorts, orderin

```bash
formo profiles get vitalik.eth --expand labels,chains,tokens
formo profiles get vitalik.eth --timestamp 2025-06-21T10:03:00Z
formo profiles search --order-by net_worth_usd --order-dir desc --size 10
formo profiles search \
--filters '[{"field":"users.net_worth_usd","op":"gt","value":10000}]' \
Expand All @@ -82,6 +83,8 @@ Always use canonical filter paths: `users.<attribute>` (e.g. `users.net_worth_us

Profile updates, label changes, and wallet imports require `profiles:write`. Preserve pagination metadata and continue only while `has_more` is true.

Use `--timestamp <ISO-8601>` with `profiles get` to return the stored wallet-enrichment snapshot closest to that instant. `profiles search --timestamp` supports the same lookup but requires `--address`. If two snapshots are equally close, the later snapshot is returned. Project engagement fields, identity overrides, and labels remain current.

## Manage Formo resources

Use CLI command groups for operational resources:
Expand Down
31 changes: 31 additions & 0 deletions src/commands/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface LifecycleThresholdOptions {

export interface GetProfileOptions extends LifecycleThresholdOptions {
expand?: string
timestamp?: string
}

function addLifecycleThresholdParams(
Expand Down Expand Up @@ -87,6 +88,11 @@ const lifecycleThresholdOptions = {
.describe('Override lifecycle at-risk prior active days threshold'),
}

const profileTimestampOption = z
.string()
.datetime({ offset: true })
.optional()

export function getProfileRun(
address: string,
optionsOrExpand: GetProfileOptions | string = {},
Expand All @@ -99,6 +105,7 @@ export function getProfileRun(
: optionsOrExpand
const params: Record<string, string | number> = {}
if (options.expand) params.expand = options.expand
if (options.timestamp) params.timestamp = options.timestamp
addLifecycleThresholdParams(params, options)
return client.get(`/v0/profiles/${encodeURIComponent(address)}`, { params })
}
Expand All @@ -113,6 +120,9 @@ profiles.command('get', {
.string()
.optional()
.describe('Comma-separated list of fields to expand: apps,chains,tokens,labels'),
timestamp: profileTimestampOption.describe(
'Return the wallet-enrichment snapshot closest to this ISO-8601 timestamp',
),
...lifecycleThresholdOptions,
}),
examples: [
Expand All @@ -122,6 +132,11 @@ profiles.command('get', {
options: { expand: 'labels,chains' },
description: 'Get profile with expanded labels and chains',
},
{
args: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
options: { timestamp: '2025-06-21T10:03:00Z' },
description: 'Get the closest stored wallet-enrichment snapshot',
},
],
hint: 'Requires profiles:read scope on your API key.',
run({ args, options }) {
Expand All @@ -132,6 +147,7 @@ profiles.command('get', {
export interface SearchProfilesOptions extends LifecycleThresholdOptions {
address?: string
search?: string
timestamp?: string
page?: number
size?: number
orderBy?: string
Expand Down Expand Up @@ -326,12 +342,17 @@ export function parseSearchFilters(raw: string): unknown[] {
}

export function searchProfilesRun(options: SearchProfilesOptions) {
if (options.timestamp && !options.address) {
throw new Error('--timestamp requires --address')
}

requireApiKey()
const client = createClient()

const params: Record<string, string | number> = {}
if (options.address) params.address = options.address
if (options.search) params.search = options.search
if (options.timestamp) params.timestamp = options.timestamp
if (options.page !== undefined) params.page = options.page
if (options.size !== undefined) params.size = options.size
if (options.orderBy) params.order_by = options.orderBy
Expand Down Expand Up @@ -361,6 +382,9 @@ profiles.command('search', {
options: z.object({
address: z.string().optional().describe('Filter by wallet address'),
search: z.string().optional().describe('Free-text search across address and identity fields'),
timestamp: profileTimestampOption.describe(
'Return the closest wallet-enrichment snapshot; requires --address',
),
page: z.coerce
.number()
.int()
Expand Down Expand Up @@ -423,6 +447,13 @@ profiles.command('search', {
}),
examples: [
{ options: { size: 10 }, description: 'List first 10 profiles' },
{
options: {
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
timestamp: '2025-06-21T10:03:00Z',
},
description: 'Search the closest stored wallet-enrichment snapshot',
},
{
options: { orderBy: 'net_worth_usd', orderDir: 'desc', size: 5 },
description: 'Top 5 profiles by net worth',
Expand Down
6 changes: 6 additions & 0 deletions test/commands/profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ describe('commands/profiles', function () {
searchProfilesRun({ filters: '{"field":"x"}' }),
).to.throw(/filters/);
});

it('requires an address for historical snapshot searches', function () {
expect(() =>
searchProfilesRun({ timestamp: '2025-06-21T10:03:00Z' }),
).to.throw(/timestamp.*address/);
});
});

describe('updateProfileRun() — local validation', function () {
Expand Down