opensea-sdk: a read-only mirror whose main trap is response casing
TypeScript SDK for the OpenSea marketplace
At a glance
- What is it?
- opensea-sdk is the TypeScript client for the OpenSea marketplace, and its sharpest edge is a response casing rewrite that compiles cleanly against the wire types. The public repository is a read-only mirror of a private monorepo, so a fix you write can sit behind the published tree for weeks.
- Who is it for?
- opensea-sdk fits code that has to read and act on the OpenSea orderbook from TypeScript, and it fits badly if you need to send a patch upstream or trust response shapes without reading the casing rules. The grouped api object, the off-chain then on-chain order flow and the optional viem entry are what earn the dependency weight.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 14, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Pull requests on the public mirror are read, never merged
The package is built in a private monorepo and copied into ProjectOpenSea/opensea-sdk when a version ships, so the public tree can sit behind the internal main branch by weeks. The last push was on 2026-09-26. A pull request opened against the mirror cannot be merged there: it is read, and a fix judged worth taking is recreated inside the monorepo instead. Because a change that has already landed internally stays invisible until the next version ships, filing an issue before writing a patch is the quickest way to find out whether the bug you are chasing has been fixed already. That makes this repository a place to report defects rather than to contribute code, and it puts a lag between a diagnosis and a fix you can pin. Three versions in the recent record were cut on that same day, v12.9.2 at 17:35:53Z, v12.10.0 at 19:32:00Z and v12.10.1 at 22:59:41Z, so the visible cadence is a cluster on one day rather than a steady stream.
Every response key arrives camelized while the wire stays snake_case
The API speaks snake_case, and the SDK rewrites keys in both directions: response keys arrive camelized, while camelCase request bodies and query params are converted to snake_case on the way out. You read collection.bannerImageUrl and banner_image_url is what crosses the wire. Wire shapes come from @opensea/api-types, which keeps them snake_case, and pairing one of those types with a value the SDK returned compiles without complaint. Nothing then throws; every snake_case field on that object reads undefined at runtime.
import type { AccountResolveResponse } from "@opensea/api-types";
// Wrong. This compiles, and every snake_case field reads undefined at runtime.
const account: AccountResolveResponse = await sdk.api.accounts.resolveAccount("vitalik.eth");
console.log(account.ens_name); // undefined; the runtime key is `ensName`Of the 166 schemas in the spec that declare snake_case properties directly, 15 accept a camelized value with no error, and AccountResolveResponse sits among them. Its single required property is address, a name with no dash to rewrite, so nothing required is missing and the camelCase keys slip through as permitted extras. The compiler rejects most of that set and stays quiet on a slice of it. Two spellings clear the problem: the SDK's own response type, already camelized, or the wire type wrapped in Camelize when no alias exists for that response.
import type { ResolveAccountResponse } from "@opensea/sdk";
const account: ResolveAccountResponse = await sdk.api.accounts.resolveAccount("vitalik.eth");
console.log(account.ensName);Collection traits break when the casing rewrite is left on
camelizeResponse: false turns the rewrite off for a single call, and the documentation says to use it where the response keys are data rather than field names. getTraits is the named case. It is keyed by collection-authored trait names, so the rewrite would report a dark_brown trait as darkBrown and merge two traits that differ only in casing, a real hazard for anyone filtering on trait values. The SDK cannot know which strings a collection author meant as field names, so it camelizes all of them.
type UploadPolicy = { upload_url: string; success_action_status: string };
const policy = await sdk.api.get<UploadPolicy>("/api/v2/some/path", undefined, {
camelizeResponse: false,
});
console.log(policy.success_action_status); // typed, and present at runtime
// @ts-expect-error the rewrite did not run, so there is no `successActionStatus`
console.log(policy.successActionStatus);With the literal in place, api.get, api.post and api.request overload on it and hand back the raw T, so the declared type is the wire shape the call really returns. In the block above, policy.success_action_status is typed and present at runtime, and the final line is a @ts-expect-error because successActionStatus was never created.
A widened boolean keeps the camelCase type but skips the rewrite
Write the flag as the literal false at the call site, or declare the options object as const. TypeScript preserves the literal only in those two positions, and any other spelling widens it to boolean, which is the position with no overload and no accurate type.
type UploadPolicy = { upload_url: string; success_action_status: string };
declare const skipRewrite: boolean;
// A boolean whose value the compiler cannot see.
await sdk.api.get<UploadPolicy>("/api/v2/some/path", undefined, {
camelizeResponse: skipRewrite,
});
// A `const` declaration widens `false` to `boolean`; `as const` keeps the literal.
const options = { camelizeResponse: false };
await sdk.api.get<UploadPolicy>("/api/v2/some/path", undefined, options);Both spellings inside that block compile. At runtime the rewrite is skipped whenever the value is false, so the data on hand is right, yet the declared type is Camelize<T> in each case, and that type advertises camelCase properties the payload never carried. This is the quiet failure of the whole design: the value is correct and the type is wrong, so a mistyped property resolves to undefined behind a signature that promises otherwise. Keeping the flag inline is the only arrangement that keeps the raw-T overload selected, which is cheap to do and invisible once the code is written.
No install command appears, only an API key and a constructor
No install command appears in the README. The package names itself @opensea/sdk in package.json and carries an npm version badge, and the documented path runs straight from a key to a constructor, so nothing in the file hands you a package manager line to copy. The fastest key needs no signup: request it in code and it stays valid for 7 days.
import { ethers } from "ethers";
import { OpenSeaSDK, Chain } from "@opensea/sdk";
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY");
const { apiKey } = await OpenSeaSDK.requestInstantApiKey();
const sdk = new OpenSeaSDK(provider, { chain: Chain.Mainnet, apiKey });That call hands back an apiKey value which goes straight into the OpenSeaSDK constructor next to a chain, and the provider is your own RPC endpoint, which the SDK never supplies for you. From a shell, the same key comes out of a POST to the keys endpoint:
curl -s -X POST https://api.opensea.io/api/v2/auth/keys | jq -r '.api_key'For anything outliving a week, permanent keys are created at opensea.io/settings/developer. They do not expire, carry higher rate limits, and can be rotated from the account. With an instance in hand, the first useful read is a grouped call: sdk.api.collections.getCollection(slug), sdk.api.nfts.getNFT(...), sdk.api.offers.getAllOffers(...), spread across accounts, assets, chains, drops, events, listings, orders, tokens, transactions and walletAuth, while search stays flat as sdk.api.search(args). Creating an order runs off-chain, fulfilling it runs onchain, and the SDK emits events through both.
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { OpenSeaSDK, Chain } from '@opensea/sdk/viem'viem callers import from the @opensea/sdk/viem entry point rather than the root package. package.json declares that path in its exports map next to the root and ./stream, and lists viem as optional in peerDependenciesMeta.
ethers ships as a hard dependency even through the viem entry
The dependency list explains what that choice costs. ethers is a regular dependency at ^6.16.0 rather than a peer, so a viem-only application installs it anyway, and it arrives together with @opensea/seaport-js at ^4.2.0 and @noble/hashes at ^2.0.1. @opensea/api-types at ^0.14.0 is a direct dependency too, which is what puts the snake_case wire definitions and the camelized runtime objects on two separate package version lines. viem sits in peerDependencies at ^2.0.0 with optional set to true, so no build forces that install on anyone. Publishing config points the same way: main resolves to lib/index.js, types resolves to lib/index.d.ts, and the files array ships the compiled output alongside the TypeScript sources, so a reader of node_modules can trace a wrong field back to the call that produced it.
Contributor tooling outruns what the mirror will accept
The tree carries a contradiction worth stating plainly. The mirror refuses pull requests, yet the root holds CONTRIBUTING.md, AGENTS.md, SECURITY.md, a .github directory, renovate.json and biome.json, and package.json defines a full contributor command set: lint and lint:fix both run biome check, format and format:check run biome, test runs vitest run, test:integration runs vitest against vitest.integration.config.ts, test:all drives both through concurrently, and test:coverage runs vitest run --coverage. The dev dependencies back that up with @vitest/coverage-v8 at ^3.2.4, concurrently at ^9.2.1 and @types/node at ^25.4.0. None of that machinery gets a change merged, because policy routes fixes to a repository you cannot read. What it does buy is a way to reproduce a failure against the published build before reporting it, which is the practical content of the advice to file an issue first and write the patch second.
Deprecated flat methods survive until the next major version
The other friction sits next to the grouped calls. The flat form sdk.api.getCollection(slug) still works, carries an @deprecated tag naming its replacement, and is scheduled for removal in the next major. The four calls whose names change are not listed in the README, which links to the calling-the-api anchor in developerDocs/api-reference.md and leaves the mapping there. Anyone moving a working integration to a new major has to read that page first, or find the breakage from the compiler after the upgrade is already merged. Until that major ships, both spellings coexist and the old one costs nothing beyond strikethrough text in an editor.
Editorial conclusion
opensea-sdk fits code that has to read and act on the OpenSea orderbook from TypeScript, and it fits badly if you need to send a patch upstream or trust response shapes without reading the casing rules. The grouped api object, the off-chain then on-chain order flow and the optional viem entry are what earn the dependency weight. Verify first that camelizeResponse: false stays inline, because a widened boolean leaves the declared type claiming properties the payload lacks. The published v12.10.1 package carries the MIT licence.
Frequently asked questions
How do I get an opensea-sdk API key without signing up?
Call OpenSeaSDK.requestInstantApiKey() in code, or post to the keys endpoint from a shell, and the returned key is valid for 7 days. Permanent keys are created at opensea.io/settings/developer, do not expire, carry higher rate limits, and can be rotated from the account.
Why does account.ens_name read undefined after sdk.api.accounts.resolveAccount?
The SDK rewrites response keys to camelCase, so the runtime key is ensName. Annotating the value with the wire type AccountResolveResponse from @opensea/api-types compiles anyway, because its single required property is address and nothing required goes missing.
When should I pass camelizeResponse: false in opensea-sdk?
Set the literal false where the response keys are data rather than field names, which the documentation names as the getTraits case. Keep it inline at the call site, because a widened boolean or a plain const object still skips the rewrite at runtime while the declared type stays Camelize<T>.
Does opensea-sdk require ethers when I use viem?
ethers is a regular dependency at ^6.16.0, so it is installed either way, while viem is an optional peer dependency at ^2.0.0. The viem build imports from the @opensea/sdk/viem entry point declared in the exports map.
Can I merge a pull request into ProjectOpenSea/opensea-sdk?
No. The repository is described as a read-only mirror of a private monorepo, and pull requests opened there cannot be merged; a fix judged worth taking is recreated in the monorepo. Filing an issue first also tells you whether the bug is already fixed internally.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/projectopensea-opensea-sdk)