Skip to content

Commit 45a1ad5

Browse files
Release v2.12.0
Origin-SHA: 9d2722bd9a483bb8d76d9e290c3094ca172e1c3b ### Minor Changes - 92926ec: Add the collection group NFT search and facets endpoints (`GET /api/v2/nfts/group/{group}` and `GET /api/v2/nfts/group/{group}/facets`), which search NFTs across every collection in a group, such as `trading_cards`, by normalized attributes (franchise, grader, grade, subject, year, set, language, edition, product type). In the SDK, `api.nfts.getNFTsByGroup(group, args?)` and `api.nfts.getNFTGroupFacets(group, args?)` take `GetNFTsByGroupArgs` and `GetNFTGroupFacetsArgs` and return `GetNFTsByGroupResponse` and `GetNFTGroupFacetsResponse`. Attribute filters are string arrays sent as repeated query parameters, since a comma-joined value is matched as one literal. In the CLI, `opensea nfts list-by-group <group>` and `opensea nfts group-facets <group>` take repeatable attribute flags (`--grader PSA --grader CGC`), and the programmatic client gains `nfts.listByGroup` and `nfts.groupFacets`. The CLI HTTP client now sends an array query value as repeated parameters instead of `String(array)`. No existing command passed an array; they join comma-separated values themselves. ### Patch Changes - Updated dependencies [92926ec] - @opensea/sdk@12.12.0 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent bc30322 commit 45a1ad5

14 files changed

Lines changed: 453 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# @opensea/cli
22

3+
## 2.12.0
4+
5+
### Minor Changes
6+
7+
- 92926ec: Add the collection group NFT search and facets endpoints (`GET /api/v2/nfts/group/{group}` and `GET /api/v2/nfts/group/{group}/facets`), which search NFTs across every collection in a group, such as `trading_cards`, by normalized attributes (franchise, grader, grade, subject, year, set, language, edition, product type).
8+
9+
In the SDK, `api.nfts.getNFTsByGroup(group, args?)` and `api.nfts.getNFTGroupFacets(group, args?)` take `GetNFTsByGroupArgs` and `GetNFTGroupFacetsArgs` and return `GetNFTsByGroupResponse` and `GetNFTGroupFacetsResponse`. Attribute filters are string arrays sent as repeated query parameters, since a comma-joined value is matched as one literal.
10+
11+
In the CLI, `opensea nfts list-by-group <group>` and `opensea nfts group-facets <group>` take repeatable attribute flags (`--grader PSA --grader CGC`), and the programmatic client gains `nfts.listByGroup` and `nfts.groupFacets`. The CLI HTTP client now sends an array query value as repeated parameters instead of `String(array)`. No existing command passed an array; they join comma-separated values themselves.
12+
13+
### Patch Changes
14+
15+
- Updated dependencies [92926ec]
16+
- @opensea/sdk@12.12.0
17+
318
## 2.11.3
419

520
### Patch Changes

‎docs/cli-reference.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,21 @@ opensea nfts get <chain> <contract> <token-id>
175175
opensea nfts list-by-collection <slug> [--limit <n>] [--next <cursor>]
176176
opensea nfts list-by-contract <chain> <contract> [--limit <n>] [--next <cursor>]
177177
opensea nfts list-by-account <chain> <address> [--limit <n>] [--next <cursor>] [--include-auto-hidden]
178+
opensea nfts list-by-group <group> [--franchise <v>] [--grader <v>] [--grade <v>] [--subject <v>] [--year <v>] [--set <v>] [--language <v>] [--edition <v>] [--product-type <v>] [--collection-slug <slug>] [--query <text>] [--is-listed <true|false>] [--min-estimated-value-usd <usd>] [--max-estimated-value-usd <usd>] [--sort-by <field>] [--sort-direction <dir>] [--limit <n>] [--next <cursor>]
179+
opensea nfts group-facets <group> [--facet-type <type>] [--collection-slug <slug>] [attribute filters]
178180
opensea nfts refresh <chain> <contract> <token-id>
179181
opensea nfts contract <chain> <address>
180182
```
181183

182184
`--include-auto-hidden` also returns NFTs hidden automatically because a third party minted or sent them to the account. NFTs the holder hid themselves stay hidden, and NFTs removed for policy violations are not returned.
183185

186+
`list-by-group` searches every collection in a group, such as `trading_cards`, by normalized attributes. The attribute flags, `--collection-slug` and `--facet-type` are repeatable: values of one flag match any of them, and different flags are AND-combined. Use `group-facets` to discover the values and their counts.
187+
188+
```bash
189+
opensea nfts group-facets trading_cards --facet-type grader --franchise Pokemon
190+
opensea nfts list-by-group trading_cards --franchise Pokemon --grader PSA --grader CGC --grade 10 --is-listed true --sort-by price --sort-direction asc
191+
```
192+
184193
## Listings
185194

186195
```bash

‎docs/sdk.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ const { nfts, next } = await client.nfts.listByAccount("ethereum", "0x123...", {
5757
limit: 10,
5858
})
5959

60+
const { nfts, next } = await client.nfts.listByGroup("trading_cards", {
61+
franchise: ["Pokemon"],
62+
grader: ["PSA", "CGC"],
63+
isListed: true,
64+
limit: 10,
65+
})
66+
67+
const { facets } = await client.nfts.groupFacets("trading_cards", {
68+
facetTypes: ["grader"],
69+
})
70+
6071
await client.nfts.refresh("ethereum", "0x123...", "1")
6172

6273
const contract = await client.nfts.getContract("ethereum", "0x123...")

‎package-lock.json‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "2.11.3",
3+
"version": "2.12.0",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",
@@ -24,7 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@opensea/api-types": "^0.16.1",
27-
"@opensea/sdk": "^12.11.4",
27+
"@opensea/sdk": "^12.12.0",
2828
"@opensea/wallet-adapters": "^1.2.1",
2929
"commander": "^14.0.3",
3030
"zod": "^4.3.6"

‎src/client.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,15 @@ function parseRetryAfter(header: string | null): number | undefined {
7474
function appendParams(url: URL, params?: Record<string, unknown>): void {
7575
if (!params) return
7676
for (const [key, value] of Object.entries(params)) {
77-
if (value !== undefined && value !== null) {
77+
if (Array.isArray(value)) {
78+
// Repeated, not comma-joined: `?grader=PSA&grader=CGC`. Endpoints that
79+
// want one comma-separated value get a joined string from the caller.
80+
for (const item of value) {
81+
if (item !== undefined && item !== null) {
82+
url.searchParams.append(key, String(item))
83+
}
84+
}
85+
} else if (value !== undefined && value !== null) {
7886
url.searchParams.set(key, String(value))
7987
}
8088
}

‎src/commands/collections.ts‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
addChainOption,
99
addPaginationOptions,
1010
addSortDirectionOption,
11+
parseBooleanOption,
1112
parseIntOption,
1213
readJsonBodyOption,
1314
} from "../parse.js"
@@ -36,13 +37,6 @@ const PAGE_MEDIA_PLACEMENTS = [
3637
"team",
3738
]
3839

39-
function parseBooleanOption(value: string, flag: string): boolean {
40-
if (value !== "true" && value !== "false") {
41-
throw new Error(`${flag} must be 'true' or 'false'`)
42-
}
43-
return value === "true"
44-
}
45-
4640
export function collectionsCommand(
4741
getClient: () => OpenSeaClient,
4842
getFormat: () => OutputFormat,

‎src/commands/nfts.ts‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type { OutputFormat } from "../output.js"
44
import { formatOutput, outputGet } from "../output.js"
55
import {
66
addPaginationOptions,
7+
addSortDirectionOption,
78
addTraitsOption,
9+
collectOption,
10+
parseBooleanOption,
11+
parseFloatOption,
812
parseIntOption,
913
parseTraitsOption,
1014
readJsonBodyOption,
@@ -16,6 +20,44 @@ import type {
1620
ValidateMetadataResponse,
1721
} from "../types/index.js"
1822

23+
/** [CLI flag, Commander option key, wire parameter] per normalized attribute. */
24+
const GROUP_FACET_FILTERS = [
25+
["franchise", "franchise", "franchise"],
26+
["grader", "grader", "grader"],
27+
["grade", "grade", "grade"],
28+
["subject", "subject", "subject"],
29+
["year", "year", "year"],
30+
["set", "set", "set"],
31+
["language", "language", "language"],
32+
["edition", "edition", "edition"],
33+
["product-type", "productType", "product_type"],
34+
] as const
35+
36+
type GroupFacetFilterOptions = Partial<
37+
Record<(typeof GROUP_FACET_FILTERS)[number][1], string[]>
38+
>
39+
40+
function addGroupFacetFilterOptions(cmd: Command): Command {
41+
for (const [flag, , wire] of GROUP_FACET_FILTERS) {
42+
cmd.option(
43+
`--${flag} <value>`,
44+
`Only NFTs whose normalized ${wire.replace("_", " ")} is this value (repeatable, matches any)`,
45+
collectOption,
46+
)
47+
}
48+
return cmd
49+
}
50+
51+
function groupFacetFilterParams(
52+
options: GroupFacetFilterOptions,
53+
): Record<string, string[] | undefined> {
54+
const params: Record<string, string[] | undefined> = {}
55+
for (const [, key, wire] of GROUP_FACET_FILTERS) {
56+
params[wire] = options[key]
57+
}
58+
return params
59+
}
60+
1961
export function nftsCommand(
2062
getClient: () => OpenSeaClient,
2163
getFormat: () => OutputFormat,
@@ -120,6 +162,122 @@ export function nftsCommand(
120162
},
121163
)
122164

165+
addGroupFacetFilterOptions(
166+
addSortDirectionOption(
167+
addPaginationOptions(
168+
cmd
169+
.command("list-by-group")
170+
.description(
171+
"Search NFTs across a collection group by normalized attributes",
172+
)
173+
.argument("<group>", "Collection group (e.g. trading_cards)"),
174+
"Number of results (max 100)",
175+
),
176+
)
177+
.option(
178+
"--collection-slug <slug>",
179+
"Restrict to this collection in the group (repeatable, max 10)",
180+
collectOption,
181+
)
182+
.option("--query <text>", "Full-text match on NFT name")
183+
.option("--is-listed <bool>", "Only listed (true) or unlisted (false)")
184+
.option(
185+
"--min-estimated-value-usd <usd>",
186+
"Minimum estimated value in USD, inclusive",
187+
)
188+
.option(
189+
"--max-estimated-value-usd <usd>",
190+
"Maximum estimated value in USD, inclusive",
191+
)
192+
.option(
193+
"--sort-by <field>",
194+
"Sort field (price, top_offer, last_sale, last_sale_date, created_date, received_date, listing_created_date, best_deal, estimated_value)",
195+
),
196+
).action(
197+
async (
198+
group: string,
199+
options: GroupFacetFilterOptions & {
200+
limit: string
201+
next?: string
202+
collectionSlug?: string[]
203+
query?: string
204+
isListed?: string
205+
minEstimatedValueUsd?: string
206+
maxEstimatedValueUsd?: string
207+
sortBy?: string
208+
sortDirection?: string
209+
},
210+
) => {
211+
const client = getClient()
212+
await outputGet(client, getFormat(), `/api/v2/nfts/group/${group}`, {
213+
collection_slugs: options.collectionSlug,
214+
query: options.query,
215+
is_listed:
216+
options.isListed === undefined
217+
? undefined
218+
: parseBooleanOption(options.isListed, "--is-listed"),
219+
min_estimated_value_usd:
220+
options.minEstimatedValueUsd === undefined
221+
? undefined
222+
: parseFloatOption(
223+
options.minEstimatedValueUsd,
224+
"--min-estimated-value-usd",
225+
),
226+
max_estimated_value_usd:
227+
options.maxEstimatedValueUsd === undefined
228+
? undefined
229+
: parseFloatOption(
230+
options.maxEstimatedValueUsd,
231+
"--max-estimated-value-usd",
232+
),
233+
sort_by: options.sortBy,
234+
sort_direction: options.sortDirection,
235+
limit: parseIntOption(options.limit, "--limit"),
236+
next: options.next,
237+
...groupFacetFilterParams(options),
238+
})
239+
},
240+
)
241+
242+
addGroupFacetFilterOptions(
243+
cmd
244+
.command("group-facets")
245+
.description(
246+
"Get normalized attribute values and NFT counts for a collection group",
247+
)
248+
.argument("<group>", "Collection group (e.g. trading_cards)")
249+
.option(
250+
"--facet-type <type>",
251+
"Only return this facet type, e.g. grader (repeatable)",
252+
collectOption,
253+
)
254+
.option(
255+
"--collection-slug <slug>",
256+
"Restrict counts to this collection in the group (repeatable, max 10)",
257+
collectOption,
258+
),
259+
).action(
260+
async (
261+
group: string,
262+
options: GroupFacetFilterOptions & {
263+
facetType?: string[]
264+
collectionSlug?: string[]
265+
},
266+
) => {
267+
const client = getClient()
268+
await outputGet(
269+
client,
270+
getFormat(),
271+
`/api/v2/nfts/group/${group}/facets`,
272+
{
273+
facet_types: options.facetType,
274+
collection_slugs: options.collectionSlug,
275+
...groupFacetFilterParams(options),
276+
},
277+
)
278+
},
279+
)
280+
123281
cmd
124282
.command("refresh")
125283
.description("Refresh NFT metadata")

‎src/parse.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ export function parseIntOption(value: string, name: string): number {
3636
return parsed
3737
}
3838

39+
export function parseBooleanOption(value: string, flag: string): boolean {
40+
if (value !== "true" && value !== "false") {
41+
throw new Error(`${flag} must be 'true' or 'false'`)
42+
}
43+
return value === "true"
44+
}
45+
46+
/** Commander argParser for a repeatable option: `--x a --x b` → `["a", "b"]`. */
47+
export function collectOption(value: string, previous?: string[]): string[] {
48+
return [...(previous ?? []), value]
49+
}
50+
3951
export function parseFloatOption(value: string, name: string): number {
4052
const parsed = Number(value)
4153
if (

0 commit comments

Comments
 (0)