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
9 changes: 4 additions & 5 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,11 @@ Event types: `sale`, `transfer`, `mint`, `listing`, `offer`, `trait_offer`, `col
## Search

```bash
opensea search collections <query> [--chains <chains>] [--limit <n>]
opensea search nfts <query> [--collection <slug>] [--chains <chains>] [--limit <n>]
opensea search tokens <query> [--chain <chain>] [--limit <n>]
opensea search accounts <query> [--limit <n>]
opensea search <query> [--types <types>] [--chains <chains>] [--limit <n>]
```

`--types` values (comma-separated): `collection`, `nft`, `token`, `account`

## Tokens

```bash
Expand All @@ -90,4 +89,4 @@ opensea swaps quote --from-chain <chain> --from-address <address> --to-chain <ch
opensea accounts get <address>
```

> REST list commands support cursor-based pagination. Search commands return a flat list with no cursor. See [pagination.md](pagination.md) for details.
> REST list commands support cursor-based pagination. The search command returns a flat list with no cursor. See [pagination.md](pagination.md) for details.
22 changes: 11 additions & 11 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,23 @@ opensea events by-account 0x21130e908bba2d41b63fbca7caa131285b8724f8 --limit 2
## Search

```bash
# Search for collections
opensea search collections mfers
# Search across all types (defaults to collections and tokens)
opensea search mfers

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

# Search for NFTs within a specific collection
opensea search nfts "rare" --collection tiny-dinos-eth --limit 5

# Search for tokens/currencies
opensea search tokens eth --limit 5
# Search for NFTs and collections
opensea search "cool cat" --types collection,nft --limit 5

# Search for tokens on a specific chain
opensea search tokens usdc --chain base --limit 5
opensea search usdc --types token --chains base --limit 5

# Search for accounts
opensea search accounts vitalik --limit 5
opensea search vitalik --types account --limit 5

# Search across all types on a specific chain
opensea search "ape" --types collection,nft,token,account --chains ethereum
```

## Tokens
Expand Down
2 changes: 1 addition & 1 deletion docs/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ opensea tokens trending --limit 5 --next "abc123..."
- `tokens trending`
- `tokens top`

> **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.
> **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).

## SDK

Expand Down
37 changes: 21 additions & 16 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const client = new OpenSeaCLI({ apiKey: process.env.OPENSEA_API_KEY })
|---|---|---|---|
| `apiKey` | `string` | *required* | OpenSea API key |
| `baseUrl` | `string` | `https://api.opensea.io` | API base URL override |
| `graphqlUrl` | `string` | `https://gql.opensea.io/graphql` | GraphQL URL override |
| `chain` | `string` | `"ethereum"` | Default chain |

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

## Search

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.
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).

```typescript
const collections = await client.search.collections("mfers", {
const { results } = await client.search.query("mfers", {
assetTypes: ["collection", "nft"],
chains: ["ethereum"],
limit: 5,
})

const nfts = await client.search.nfts("cool cat", {
collection: "cool-cats-nft",
chains: ["ethereum"],
limit: 5,
})

const tokens = await client.search.tokens("usdc", {
chain: "base",
limit: 5,
limit: 10,
})

const accounts = await client.search.accounts("vitalik", { limit: 5 })
// Each result has a type and the corresponding object
for (const result of results) {
switch (result.type) {
case "collection":
console.log(result.collection?.name)
break
case "nft":
console.log(result.nft?.name)
break
case "token":
console.log(result.token?.symbol)
break
case "account":
console.log(result.account?.username)
break
}
}
```

## Swaps
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opensea/cli",
"version": "0.4.0",
"version": "0.4.1",
"type": "module",
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
"main": "dist/index.js",
Expand Down
51 changes: 0 additions & 51 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import type { OpenSeaClientConfig } from "./types/index.js"

const DEFAULT_BASE_URL = "https://api.opensea.io"
const DEFAULT_GRAPHQL_URL = "https://gql.opensea.io/graphql"
const DEFAULT_TIMEOUT_MS = 30_000

export class OpenSeaClient {
private apiKey: string
private baseUrl: string
private graphqlUrl: string
private defaultChain: string
private timeoutMs: number
private verbose: boolean

constructor(config: OpenSeaClientConfig) {
this.apiKey = config.apiKey
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL
this.graphqlUrl = config.graphqlUrl ?? DEFAULT_GRAPHQL_URL
this.defaultChain = config.chain ?? "ethereum"
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS
this.verbose = config.verbose ?? false
Expand Down Expand Up @@ -104,54 +101,6 @@ export class OpenSeaClient {
return response.json() as Promise<T>
}

async graphql<T>(
query: string,
variables?: Record<string, unknown>,
): Promise<T> {
if (this.verbose) {
console.error(`[verbose] POST ${this.graphqlUrl}`)
}

const response = await fetch(this.graphqlUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"x-api-key": this.apiKey,
},
body: JSON.stringify({ query, variables }),
signal: AbortSignal.timeout(this.timeoutMs),
})

if (this.verbose) {
console.error(`[verbose] ${response.status} ${response.statusText}`)
}

if (!response.ok) {
const body = await response.text()
throw new OpenSeaAPIError(response.status, body, "graphql")
}

const json = (await response.json()) as {
data?: T
errors?: { message: string }[]
}

if (json.errors?.length) {
throw new OpenSeaAPIError(
400,
json.errors.map(e => e.message).join("; "),
"graphql",
)
}

if (!json.data) {
throw new OpenSeaAPIError(500, "GraphQL response missing data", "graphql")
}

return json.data
}

getDefaultChain(): string {
return this.defaultChain
}
Expand Down
109 changes: 25 additions & 84 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,107 +3,48 @@ import type { OpenSeaClient } from "../client.js"
import type { OutputFormat } from "../output.js"
import { formatOutput } from "../output.js"
import { parseIntOption } from "../parse.js"
import {
SEARCH_ACCOUNTS_QUERY,
SEARCH_COLLECTIONS_QUERY,
SEARCH_NFTS_QUERY,
SEARCH_TOKENS_QUERY,
} from "../queries.js"
import type {
SearchAccountResult,
SearchCollectionResult,
SearchNFTResult,
SearchTokenResult,
} from "../types/index.js"
import type { SearchResponse } from "../types/index.js"

export function searchCommand(
getClient: () => OpenSeaClient,
getFormat: () => OutputFormat,
): Command {
const cmd = new Command("search").description(
"Search for collections, NFTs, tokens, and accounts",
)

cmd
.command("collections")
.description("Search collections by name or slug")
const cmd = new Command("search")
.description("Search across collections, tokens, NFTs, and accounts")
.argument("<query>", "Search query")
.option("--chains <chains>", "Filter by chains (comma-separated)")
.option("--limit <limit>", "Number of results", "10")
.action(
async (query: string, options: { chains?: string; limit: string }) => {
const client = getClient()
const result = await client.graphql<{
collectionsByQuery: SearchCollectionResult[]
}>(SEARCH_COLLECTIONS_QUERY, {
query,
limit: parseIntOption(options.limit, "--limit"),
chains: options.chains?.split(","),
})
console.log(formatOutput(result.collectionsByQuery, getFormat()))
},
.option(
"--types <types>",
"Filter by type (comma-separated: collection,nft,token,account)",
)

cmd
.command("nfts")
.description("Search NFTs by name")
.argument("<query>", "Search query")
.option("--collection <slug>", "Filter by collection slug")
.option("--chains <chains>", "Filter by chains (comma-separated)")
.option("--limit <limit>", "Number of results", "10")
.option("--limit <limit>", "Number of results", "20")
.action(
async (
query: string,
options: { collection?: string; chains?: string; limit: string },
options: {
types?: string
chains?: string
limit: string
},
) => {
const client = getClient()
const result = await client.graphql<{
itemsByQuery: SearchNFTResult[]
}>(SEARCH_NFTS_QUERY, {
const params: Record<string, unknown> = {
query,
collectionSlug: options.collection,
limit: parseIntOption(options.limit, "--limit"),
chains: options.chains?.split(","),
})
console.log(formatOutput(result.itemsByQuery, getFormat()))
}
if (options.types) {
params.asset_types = options.types
}
if (options.chains) {
params.chains = options.chains
}
const result = await client.get<SearchResponse>(
"/api/v2/search",
params,
)
console.log(formatOutput(result, getFormat()))
},
)

cmd
.command("tokens")
.description("Search tokens/currencies by name or symbol")
.argument("<query>", "Search query")
.option("--chain <chain>", "Filter by chain")
.option("--limit <limit>", "Number of results", "10")
.action(
async (query: string, options: { chain?: string; limit: string }) => {
const client = getClient()
const result = await client.graphql<{
currenciesByQuery: SearchTokenResult[]
}>(SEARCH_TOKENS_QUERY, {
query,
limit: parseIntOption(options.limit, "--limit"),
chain: options.chain,
})
console.log(formatOutput(result.currenciesByQuery, getFormat()))
},
)

cmd
.command("accounts")
.description("Search accounts by username or address")
.argument("<query>", "Search query")
.option("--limit <limit>", "Number of results", "10")
.action(async (query: string, options: { limit: string }) => {
const client = getClient()
const result = await client.graphql<{
accountsByQuery: SearchAccountResult[]
}>(SEARCH_ACCOUNTS_QUERY, {
query,
limit: parseIntOption(options.limit, "--limit"),
})
console.log(formatOutput(result.accountsByQuery, getFormat()))
})

return cmd
}
Loading