Skip to content

Commit 7a06fc4

Browse files
ryanioclaude
andauthored
feat: replace GraphQL search with REST API search endpoint (#33)
Replace the 4 separate GraphQL search subcommands (collections, nfts, tokens, accounts) with a single unified REST endpoint (GET /api/v2/search). The new search command supports --types, --chains, and --limit options. Remove GraphQL client method, queries.ts, and graphqlUrl config since search was the only GraphQL consumer. Bump version to 0.4.1. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fa8f6da commit 7a06fc4

15 files changed

Lines changed: 207 additions & 627 deletions

File tree

docs/cli-reference.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,11 @@ Event types: `sale`, `transfer`, `mint`, `listing`, `offer`, `trait_offer`, `col
6666
## Search
6767

6868
```bash
69-
opensea search collections <query> [--chains <chains>] [--limit <n>]
70-
opensea search nfts <query> [--collection <slug>] [--chains <chains>] [--limit <n>]
71-
opensea search tokens <query> [--chain <chain>] [--limit <n>]
72-
opensea search accounts <query> [--limit <n>]
69+
opensea search <query> [--types <types>] [--chains <chains>] [--limit <n>]
7370
```
7471

72+
`--types` values (comma-separated): `collection`, `nft`, `token`, `account`
73+
7574
## Tokens
7675

7776
```bash
@@ -92,4 +91,4 @@ opensea swaps quote --from-chain <chain> --from-address <address> --to-chain <ch
9291
opensea accounts get <address>
9392
```
9493

95-
> REST list commands support cursor-based pagination. Search commands return a flat list with no cursor. See [pagination.md](pagination.md) for details.
94+
> REST list commands support cursor-based pagination. The search command returns a flat list with no cursor. See [pagination.md](pagination.md) for details.

docs/examples.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,23 @@ opensea events by-account 0x21130e908bba2d41b63fbca7caa131285b8724f8 --limit 2
8888
## Search
8989

9090
```bash
91-
# Search for collections
92-
opensea search collections mfers
91+
# Search across all types (defaults to collections and tokens)
92+
opensea search mfers
9393

94-
# Search for NFTs
95-
opensea search nfts "cool cat" --limit 5
94+
# Search for collections only
95+
opensea search "bored ape" --types collection
9696

97-
# Search for NFTs within a specific collection
98-
opensea search nfts "rare" --collection tiny-dinos-eth --limit 5
99-
100-
# Search for tokens/currencies
101-
opensea search tokens eth --limit 5
97+
# Search for NFTs and collections
98+
opensea search "cool cat" --types collection,nft --limit 5
10299

103100
# Search for tokens on a specific chain
104-
opensea search tokens usdc --chain base --limit 5
101+
opensea search usdc --types token --chains base --limit 5
105102

106103
# Search for accounts
107-
opensea search accounts vitalik --limit 5
104+
opensea search vitalik --types account --limit 5
105+
106+
# Search across all types on a specific chain
107+
opensea search "ape" --types collection,nft,token,account --chains ethereum
108108
```
109109

110110
## Tokens

docs/pagination.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ opensea tokens trending --limit 5 --next "abc123..."
3636
- `tokens trending`
3737
- `tokens top`
3838

39-
> **Note:** Search commands (`search collections`, `search nfts`, `search tokens`, `search accounts`) do not support cursor-based pagination. The underlying GraphQL API returns a flat list with no `next` cursor.
39+
> **Note:** The `search` command does not support cursor-based pagination. The search API returns a flat list with no `next` cursor; use `--limit` to control result count (max 50).
4040
4141
## SDK
4242

docs/sdk.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const client = new OpenSeaCLI({ apiKey: process.env.OPENSEA_API_KEY })
1616
|---|---|---|---|
1717
| `apiKey` | `string` | *required* | OpenSea API key |
1818
| `baseUrl` | `string` | `https://api.opensea.io` | API base URL override |
19-
| `graphqlUrl` | `string` | `https://gql.opensea.io/graphql` | GraphQL URL override |
2019
| `chain` | `string` | `"ethereum"` | Default chain |
2120

2221
## Collections
@@ -142,26 +141,32 @@ const tokenDetails = await client.tokens.get("base", "0x123...")
142141

143142
## Search
144143

145-
Search methods use GraphQL and return different result shapes than the REST API. Search endpoints do not currently expose a `next` cursor for pagination; use `limit` to control result count.
144+
Search uses the unified `/api/v2/search` REST endpoint. Results are ranked by relevance and each result has a `type` discriminator (`collection`, `nft`, `token`, or `account`) with the corresponding typed object. The search endpoint does not support cursor-based pagination; use `limit` to control result count (max 50).
146145

147146
```typescript
148-
const collections = await client.search.collections("mfers", {
147+
const { results } = await client.search.query("mfers", {
148+
assetTypes: ["collection", "nft"],
149149
chains: ["ethereum"],
150-
limit: 5,
151-
})
152-
153-
const nfts = await client.search.nfts("cool cat", {
154-
collection: "cool-cats-nft",
155-
chains: ["ethereum"],
156-
limit: 5,
157-
})
158-
159-
const tokens = await client.search.tokens("usdc", {
160-
chain: "base",
161-
limit: 5,
150+
limit: 10,
162151
})
163152

164-
const accounts = await client.search.accounts("vitalik", { limit: 5 })
153+
// Each result has a type and the corresponding object
154+
for (const result of results) {
155+
switch (result.type) {
156+
case "collection":
157+
console.log(result.collection?.name)
158+
break
159+
case "nft":
160+
console.log(result.nft?.name)
161+
break
162+
case "token":
163+
console.log(result.token?.symbol)
164+
break
165+
case "account":
166+
console.log(result.account?.username)
167+
break
168+
}
169+
}
165170
```
166171

167172
## Swaps

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",

src/client.ts

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import type { OpenSeaClientConfig } from "./types/index.js"
22

33
const DEFAULT_BASE_URL = "https://api.opensea.io"
4-
const DEFAULT_GRAPHQL_URL = "https://gql.opensea.io/graphql"
54
const DEFAULT_TIMEOUT_MS = 30_000
65

76
export class OpenSeaClient {
87
private apiKey: string
98
private baseUrl: string
10-
private graphqlUrl: string
119
private defaultChain: string
1210
private timeoutMs: number
1311
private verbose: boolean
1412

1513
constructor(config: OpenSeaClientConfig) {
1614
this.apiKey = config.apiKey
1715
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL
18-
this.graphqlUrl = config.graphqlUrl ?? DEFAULT_GRAPHQL_URL
1916
this.defaultChain = config.chain ?? "ethereum"
2017
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS
2118
this.verbose = config.verbose ?? false
@@ -104,54 +101,6 @@ export class OpenSeaClient {
104101
return response.json() as Promise<T>
105102
}
106103

107-
async graphql<T>(
108-
query: string,
109-
variables?: Record<string, unknown>,
110-
): Promise<T> {
111-
if (this.verbose) {
112-
console.error(`[verbose] POST ${this.graphqlUrl}`)
113-
}
114-
115-
const response = await fetch(this.graphqlUrl, {
116-
method: "POST",
117-
headers: {
118-
"Content-Type": "application/json",
119-
Accept: "application/json",
120-
"x-api-key": this.apiKey,
121-
},
122-
body: JSON.stringify({ query, variables }),
123-
signal: AbortSignal.timeout(this.timeoutMs),
124-
})
125-
126-
if (this.verbose) {
127-
console.error(`[verbose] ${response.status} ${response.statusText}`)
128-
}
129-
130-
if (!response.ok) {
131-
const body = await response.text()
132-
throw new OpenSeaAPIError(response.status, body, "graphql")
133-
}
134-
135-
const json = (await response.json()) as {
136-
data?: T
137-
errors?: { message: string }[]
138-
}
139-
140-
if (json.errors?.length) {
141-
throw new OpenSeaAPIError(
142-
400,
143-
json.errors.map(e => e.message).join("; "),
144-
"graphql",
145-
)
146-
}
147-
148-
if (!json.data) {
149-
throw new OpenSeaAPIError(500, "GraphQL response missing data", "graphql")
150-
}
151-
152-
return json.data
153-
}
154-
155104
getDefaultChain(): string {
156105
return this.defaultChain
157106
}

src/commands/search.ts

Lines changed: 25 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,107 +3,48 @@ import type { OpenSeaClient } from "../client.js"
33
import type { OutputFormat } from "../output.js"
44
import { formatOutput } from "../output.js"
55
import { parseIntOption } from "../parse.js"
6-
import {
7-
SEARCH_ACCOUNTS_QUERY,
8-
SEARCH_COLLECTIONS_QUERY,
9-
SEARCH_NFTS_QUERY,
10-
SEARCH_TOKENS_QUERY,
11-
} from "../queries.js"
12-
import type {
13-
SearchAccountResult,
14-
SearchCollectionResult,
15-
SearchNFTResult,
16-
SearchTokenResult,
17-
} from "../types/index.js"
6+
import type { SearchResponse } from "../types/index.js"
187

198
export function searchCommand(
209
getClient: () => OpenSeaClient,
2110
getFormat: () => OutputFormat,
2211
): Command {
23-
const cmd = new Command("search").description(
24-
"Search for collections, NFTs, tokens, and accounts",
25-
)
26-
27-
cmd
28-
.command("collections")
29-
.description("Search collections by name or slug")
12+
const cmd = new Command("search")
13+
.description("Search across collections, tokens, NFTs, and accounts")
3014
.argument("<query>", "Search query")
31-
.option("--chains <chains>", "Filter by chains (comma-separated)")
32-
.option("--limit <limit>", "Number of results", "10")
33-
.action(
34-
async (query: string, options: { chains?: string; limit: string }) => {
35-
const client = getClient()
36-
const result = await client.graphql<{
37-
collectionsByQuery: SearchCollectionResult[]
38-
}>(SEARCH_COLLECTIONS_QUERY, {
39-
query,
40-
limit: parseIntOption(options.limit, "--limit"),
41-
chains: options.chains?.split(","),
42-
})
43-
console.log(formatOutput(result.collectionsByQuery, getFormat()))
44-
},
15+
.option(
16+
"--types <types>",
17+
"Filter by type (comma-separated: collection,nft,token,account)",
4518
)
46-
47-
cmd
48-
.command("nfts")
49-
.description("Search NFTs by name")
50-
.argument("<query>", "Search query")
51-
.option("--collection <slug>", "Filter by collection slug")
5219
.option("--chains <chains>", "Filter by chains (comma-separated)")
53-
.option("--limit <limit>", "Number of results", "10")
20+
.option("--limit <limit>", "Number of results", "20")
5421
.action(
5522
async (
5623
query: string,
57-
options: { collection?: string; chains?: string; limit: string },
24+
options: {
25+
types?: string
26+
chains?: string
27+
limit: string
28+
},
5829
) => {
5930
const client = getClient()
60-
const result = await client.graphql<{
61-
itemsByQuery: SearchNFTResult[]
62-
}>(SEARCH_NFTS_QUERY, {
31+
const params: Record<string, unknown> = {
6332
query,
64-
collectionSlug: options.collection,
6533
limit: parseIntOption(options.limit, "--limit"),
66-
chains: options.chains?.split(","),
67-
})
68-
console.log(formatOutput(result.itemsByQuery, getFormat()))
34+
}
35+
if (options.types) {
36+
params.asset_types = options.types
37+
}
38+
if (options.chains) {
39+
params.chains = options.chains
40+
}
41+
const result = await client.get<SearchResponse>(
42+
"/api/v2/search",
43+
params,
44+
)
45+
console.log(formatOutput(result, getFormat()))
6946
},
7047
)
7148

72-
cmd
73-
.command("tokens")
74-
.description("Search tokens/currencies by name or symbol")
75-
.argument("<query>", "Search query")
76-
.option("--chain <chain>", "Filter by chain")
77-
.option("--limit <limit>", "Number of results", "10")
78-
.action(
79-
async (query: string, options: { chain?: string; limit: string }) => {
80-
const client = getClient()
81-
const result = await client.graphql<{
82-
currenciesByQuery: SearchTokenResult[]
83-
}>(SEARCH_TOKENS_QUERY, {
84-
query,
85-
limit: parseIntOption(options.limit, "--limit"),
86-
chain: options.chain,
87-
})
88-
console.log(formatOutput(result.currenciesByQuery, getFormat()))
89-
},
90-
)
91-
92-
cmd
93-
.command("accounts")
94-
.description("Search accounts by username or address")
95-
.argument("<query>", "Search query")
96-
.option("--limit <limit>", "Number of results", "10")
97-
.action(async (query: string, options: { limit: string }) => {
98-
const client = getClient()
99-
const result = await client.graphql<{
100-
accountsByQuery: SearchAccountResult[]
101-
}>(SEARCH_ACCOUNTS_QUERY, {
102-
query,
103-
limit: parseIntOption(options.limit, "--limit"),
104-
})
105-
console.log(formatOutput(result.accountsByQuery, getFormat()))
106-
})
107-
10849
return cmd
10950
}

0 commit comments

Comments
 (0)