TypeScript SDK for buying, selling, and managing NFTs and tokens on OpenSea. Supports ethers and viem providers.
cd packages/sdk
pnpm run build
pnpm run test
pnpm run test:integration # needs a .env, see test/README-integration.md
pnpm run check-types
pnpm run lint- Provide
OpenSeaSDK(ethers) andOpenSeaViemSDK(viem) entry points. - Provide the Stream API client at the
@opensea/sdk/streamsubpath. - Camelize API responses and expose typed helpers for orders, fulfillment, assets, and wallet auth.
- Keep the
Chainenum in sync withChainIdentifierfrom@opensea/api-types.
-
Never hand-roll API request/response types. Import from
@opensea/api-types(or re-export throughsrc/api/types.ts) using canonical schema names. -
Chain enum sync is compile-time enforced. Adding a
ChainIdentifierwithout a matchingChainvalue or payment-token case failspnpm check-types. Updatescripts/chain-data.jsonat the monorepo root and runpnpm sync-chainswhen adding chains. -
Dual provider support. Changes to
BaseOpenSeaSDKaffect both ethers and viem paths; update both provider adapters if provider-specific logic changes. -
OAuth token contract.
OpenSeaOAuthrequestsoffline_access; refresh responses may omit rotation — keep the previous refresh token. The top-levelwalletJWT claim is wallet identity;subis an account id. -
No secret leakage. API keys live in
OpenSeaAPIConfig.apiKey; never log them. -
Auth scopes are coupled to the spec in both directions.
src/scopes.tsasserts at compile time thatOPENSEA_SCOPESmatchesAuthScopefrom@opensea/api-types, so a new scope has to land in the spec and in the constant in the same commit, and that commit cannot pass theMirror layoutgate until api-types publishes. Read Spec changes and release order before starting. -
Stream client is subpath-only and dependency-free.
src/stream/is exported from./streamand never fromsrc/index.ts—EventType,Trait,TraitOfferEvent, andCollectionOfferEventexist in both surfaces with different shapes.src/stream/transport/stays internal so a non-Phoenix Stream v2 can replace it without a breaking release; client code usesStreamTransport, neverPhoenixChannelsTransport. The built entry resolves to six local files with zero external requires, so check the require graph before adding an import. Live tests are intest/integration/stream.spec.tsand need real network access, seetest/README-integration.md. -
A new sub-client method is reached through its namespace, and needs no forwarder. Each sub-client is a public property on
OpenSeaAPI(api.collections,api.tokens, …), so adding a method toCollectionsAPImakes it callable immediately. Do not add a matching flatapi.getX()method: the 88 that exist are deprecated and go in the next major, and a new one would need its own deprecation note on the day it landed.searchis the one domain with no namespace, becauseSearchAPI's only method is also calledsearchand the property would have to displace the workingapi.search()call. Three tests hold this together, all readingtest/utils/forwarderContract.ts:subclientReachability.spec.tsasserts every sub-client is namespaced or a documented exception (with a compile-timePickthat catches a namespace markedprivate, which no runtime check can see) and that the deprecated flat surface has not lost a method;forwarderSignatures.spec.tsfailscheck-typesif a surviving forwarder's parameters or return type drift from the method it delegates to. When the major removes the flat methods, deleteDEPRECATED_FLAT_SURFACE,FORWARDED_ASandforwarderSignatures.spec.tswith them — they exist only to guard that layer. Background: opensea-sdk#2007, where a method shipped with no forwarder and so no way to call it. -
Responses are camelCase;
@opensea/api-typesis snake_case. A fetcher method returnsCamelize<T>, so a raw wire type used to annotate an SDK return value is the wrong shape at runtime, and the compiler only rejects it where the wire type has a required snake_case key somewhere in its tree. Export a camelized alias insrc/api/types.tsfor a new response, or point the caller atCamelize<WireType>, which the root now exports. The exception iscamelizeResponse: false, for a response keyed by data rather than field names:get,postandrequestcarry a second signature keyed on that literal, which returns the rawTbecause the rewrite did not run. Abooleanvariable or aconst-widened options object does not select it and still getsCamelize<T>, which is the one spelling where the declared type can still be wrong.test/api/responseCasing.spec.tspins every call shape, and the README's "Response casing" section is type-checked bypnpm run check-doc-examples.
- CommonJS (
"type": "commonjs") for broad consumer support. - Node 22+ is the floor. The stream client relies on a global
WebSocket. viemis an optional peer dependency; main entry uses ethers.- Prefer
stringfor decimalAmountvalues.