@medialane/sdk
Framework-agnostic TypeScript SDK for the Medialane API. Bundles a full REST client and on-chain marketplace helpers in one package.
Install
# bun bun add @medialane/sdk starknet # npm npm install @medialane/sdk starknet # yarn yarn add @medialane/sdk starknet
Peer dependency: starknet@^6
Configure
Create a MedialaneClient with your network and API key.
import { MedialaneClient } from "@medialane/sdk"
const client = new MedialaneClient({
network: "mainnet", // "mainnet" | "sepolia"
rpcUrl: "https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_7/YOUR_KEY",
backendUrl: "https://medialane-backend-production.up.railway.app",
apiKey: "ml_live_YOUR_KEY",
marketplaceContract: "0x04299b51289aa700de4ce19cc77bcea8430bfd1aef04193efab09d60a3a7ee0f",
// Optional: configure retry for transient failures
retryOptions: {
maxAttempts: 3, // default
baseDelayMs: 300, // default
maxDelayMs: 5000, // default
},
})The apiKey is sent as x-api-key on every request. Get your key at /account.
Minting & Launchpad
The SDK provides two ways to mint assets: direct on-chain calls (requires signer) and backend-orchestrated intents.
Mint an asset into a collection
// 1. Direct on-chain (client.marketplace)
await client.marketplace.mint(account, {
collectionId: "42",
recipient: "0x0591...",
tokenUri: "ipfs://...",
})
// 2. Via backend intent (client.api)
// No SNIP-12 signing required for mint/create-collection intents
const { intentId, calls } = await client.api.createMintIntent({
owner: "0x0591...", // collection owner
collectionId: "42",
recipient: "0x0592...",
tokenUri: "ipfs://...",
})Register a new collection
// 1. Direct on-chain
await client.marketplace.createCollection(account, {
name: "My Collection",
symbol: "MYC",
baseUri: "ipfs://...",
})
// 2. Via backend intent
const { intentId, calls } = await client.api.createCollectionIntent({
owner: "0x0591...",
name: "My Collection",
symbol: "MYC",
baseUri: "ipfs://...",
})Marketplace (on-chain)
client.marketplace provides typed wrappers for direct contract calls via starknet.js.
// Get order details directly from the contract
const order = await client.marketplace.getOrderDetails("0x04f7a1...")
// Get the current nonce for signing
const nonce = await client.marketplace.getNonce("0x0591...")API Client (REST)
client.api mirrors the full REST API surface.
List open orders
const orders = await client.api.getOrders({ status: "ACTIVE", limit: 20 })
console.log(orders.data[0].orderHash, orders.data[0].price)Get a token with metadata
const token = await client.api.getToken("0x05e7...", "42")
console.log(token.data.metadata?.name)Get collections by owner
// Fetch collections owned by a wallet address
// Addresses are normalized automatically — pass any valid Starknet format
const result = await client.api.getCollectionsByOwner("0x0591...")
result.data.forEach((col) => {
console.log(col.name, col.collectionId) // collectionId = on-chain registry ID
})Create a listing intent
import { toSignatureArray } from "@medialane/sdk"
// 1. Create the intent — get typed data back
const intent = await client.api.createListingIntent({
nftContract: "0x05e7...",
tokenId: "42",
price: "500000",
currency: "USDC",
offerer: walletAddress,
endTime: Math.floor(Date.now() / 1000) + 86400 * 30,
})
// 2. Sign with starknet.js
import { Account } from "starknet"
const account = new Account(provider, walletAddress, privateKey)
const signature = await account.signMessage(intent.data.typedData)
// 3. Submit the signature
await client.api.submitIntentSignature(intent.data.id, toSignatureArray(signature))Search
const results = await client.api.search("genesis", 10)
results.data.tokens.forEach((t) => console.log(t.metadata?.name))
results.data.collections.forEach((c) => console.log(c.name))Portal — manage keys
// List your API keys
const keys = await client.api.getApiKeys()
// Create a new key
const newKey = await client.api.createApiKey("Agent Key")
console.log(newKey.data.key) // shown once — save it!
// Get usage
const usage = await client.api.getUsage()On-chain comments
// Fetch permanent on-chain comments for a token
const result = await client.api.getTokenComments("0x05e7...", "42", { limit: 20 })
result.data.forEach((c) => {
console.log(c.author, c.content, c.postedAt)
})Counter-offers
// Seller creates a counter-offer in response to a buyer's bid
const intent = await client.api.createCounterOfferIntent(
{
sellerAddress: "0x0591...",
originalOrderHash: "0x04f7a1...",
counterPrice: "750000", // raw wei
durationSeconds: 86400, // 1 day
message: "Best I can do!",
},
clerkToken
)
// Buyer fetches counter-offers for their bid
const counters = await client.api.getCounterOffers({
originalOrderHash: "0x04f7a1...",
})
console.log(counters.data[0].price)
// Buyer accepts by fulfilling the counter-offer (it is a standard listing)
await client.api.createFulfillIntent({ fulfiller: buyerAddress, orderHash: counters.data[0].orderHash })Remix licensing
import { OPEN_LICENSES } from "@medialane/sdk"
// Check if a license is open (auto-approved remix)
console.log(OPEN_LICENSES) // ["CC0", "CC BY", "CC BY-SA", "CC BY-NC"]
// Request permission to remix a token (custom offer, Clerk JWT required)
const offer = await client.api.submitRemixOffer(
{
originalContract: "0x05e7...",
originalTokenId: "42",
licenseType: "CC BY-NC",
commercial: false,
derivatives: true,
royaltyPct: 10,
message: "Would love to remix this for my EP cover",
},
clerkToken
)
// Open-license tokens are auto-approved
const autoOffer = await client.api.submitAutoRemixOffer(
{ originalContract: "0x05e7...", originalTokenId: "7", licenseType: "CC0" },
clerkToken
)
// Creator approves a pending offer
await client.api.confirmRemixOffer(offer.data.id, {
approvedCollection: "0x06a3...",
remixContract: "0x06a3...",
remixTokenId: "1",
}, clerkToken)
// Creator rejects an offer
await client.api.rejectRemixOffer(offer.data.id, clerkToken)
// Owner records their own self-remix after minting
await client.api.confirmSelfRemix(
{
originalContract: "0x05e7...",
originalTokenId: "42",
remixContract: "0x06a3...",
remixTokenId: "1",
licenseType: "CC BY",
commercial: true,
derivatives: true,
},
clerkToken
)
// List incoming / outgoing offers
const incoming = await client.api.getRemixOffers({ role: "creator" }, clerkToken)
const outgoing = await client.api.getRemixOffers({ role: "requester" }, clerkToken)
// Get public remixes for a token (no auth needed)
const remixes = await client.api.getTokenRemixes("0x05e7...", "42")
remixes.data.forEach((r) => console.log(r.remixContract, r.remixTokenId, r.licenseType))CollectionSort — typed sort options
import type { CollectionSort } from "@medialane/sdk"
// "recent" | "supply" | "floor" | "volume" | "name"
const sort: CollectionSort = "floor"
await client.api.getCollections(1, 20, true, sort)Error Handling
The SDK throws MedialaneError for marketplace issues and MedialaneApiError for REST API failures. Both carry a typed .code field from the MedialaneErrorCode union.
import { MedialaneError, MedialaneApiError } from "@medialane/sdk"
try {
await client.marketplace.mint(account, params)
} catch (err) {
if (err instanceof MedialaneError) {
console.error(err.code, err.message) // e.g. "TRANSACTION_FAILED"
}
if (err instanceof MedialaneApiError) {
console.error(err.code, err.status, err.message) // e.g. "TOKEN_NOT_FOUND", 404
}
}Error Codes
All errors expose a MedialaneErrorCode typed union:
type MedialaneErrorCode = | "TOKEN_NOT_FOUND" | "COLLECTION_NOT_FOUND" | "ORDER_NOT_FOUND" | "INTENT_NOT_FOUND" | "INTENT_EXPIRED" | "RATE_LIMITED" | "NETWORK_NOT_SUPPORTED" | "APPROVAL_FAILED" | "TRANSACTION_FAILED" | "INVALID_PARAMS" | "UNAUTHORIZED" | "UNKNOWN"
TOKEN_NOT_FOUND404 response or missing tokenCOLLECTION_NOT_FOUND404 on collection lookupORDER_NOT_FOUND404 on order lookupINTENT_NOT_FOUND404 on intent lookupINTENT_EXPIRED410 response — intent TTL exceededRATE_LIMITED429 response — too many requestsNETWORK_NOT_SUPPORTEDSepolia selected with no contract addressesAPPROVAL_FAILEDNFT approval missing before listingTRANSACTION_FAILEDOn-chain call revertedINVALID_PARAMS400 response — bad request parametersUNAUTHORIZED401/403 — missing or invalid API keyUNKNOWNUnexpected errorsNote: 4xx errors are not retried automatically. Only transient network and 5xx errors trigger the retry logic configured via retryOptions.
Full API reference — all REST endpoints, parameters, and response schemas are documented in the API Reference.