Framework-agnostic TypeScript SDK for the Medialane IP marketplace on Starknet
The Medialane SDK provides a unified interface for interacting with the Medialane marketplace: both on-chain operations (create listings, make offers, fulfill orders, mint IP assets) and REST API access (search tokens, manage orders, upload metadata to IPFS). Built for medialane.io, starknet.medialane.io, portal.medialane.io, and media-wallet.
On-Chain Operations
- Create listings (ERC-721 / ERC-1155 for sale)
- Make offers (bid with ERC-20)
- Fulfill orders (purchase NFTs)
- Cancel active orders
- Atomic multi-item cart checkout
- Built-in approval checking
- SNIP-12 typed data signing
- Mint IP NFTs into any collection
- Deploy new ERC-721 or ERC-1155 collections
REST API Client
- Query orders, tokens, collections, and activities
- Full-text search across the marketplace
- Intent-based transaction orchestration
- Upload metadata and files to IPFS (Pinata)
- Tenant portal: API keys, webhooks, usage
- ERC-1155 multi-holder ownership via
token.balances
IP Metadata Types
IpAttribute: typed OpenSea ERC-721 attributeIpNftMetadata: full IPFS metadata shape with licensing fieldsApiTokenMetadata: indexed token metadata with all licensing attributes- Berne Convention-compatible licensing data model
Developer-Friendly
- Framework-agnostic TypeScript
- Dual ESM + CJS builds
- Zod schema config validation
- Full type safety
- Peer dependency:
starknet >= 6.0.0
npm install @medialane/sdk starknet
# or
bun add @medialane/sdk starknet
# or
yarn add @medialane/sdk starknetimport { MedialaneClient } from "@medialane/sdk";
const client = new MedialaneClient({
chain: "STARKNET", // chain-scoped (default "STARKNET"); replaces `network` (v0.37.0)
rpcUrl: "https://rpc.starknet.lava.build", // optional; defaults to the chain's registry rpcUrl
backendUrl: "https://medialane-backend-production.up.railway.app", // required for .api methods
apiKey: "ml_live_...", // from Medialane Portal
});All methods require a starknet.js AccountInterface. SNIP-12 signing and waitForTransaction are handled automatically. Fulfilment is unsigned: the caller is the fulfiller, so there is no fulfiller/offerer field to pass; cancellation still signs, but without a nonce (a per-offerer counter replaces it, see incrementCounter).
Two marketplace modules are available:
client.marketplace: ERC-721 marketplace (Medialane721)client.marketplace1155: ERC-1155 marketplace (Medialane1155)
import { Account } from "starknet";
const result = await client.marketplace.createListing(account, {
nftContract: "0x05e73b7...",
tokenId: "42",
currency: "USDC",
price: "1000000", // 1 USDC (6 decimals)
durationSeconds: 86400 * 30, // 30 days
});
console.log("Listed:", result.txHash);const result = await client.marketplace.makeOffer(account, {
nftContract: "0x05e73b7...",
tokenId: "42",
currency: "USDC",
price: "500000", // 0.5 USDC
durationSeconds: 86400 * 7,
});// Fetch order details first to get paymentToken and totalPrice
const details = await client.api.getOrder(orderHash);
const result = await client.marketplace.fulfillOrder(account, {
orderHash: "0x...",
paymentToken: "0x033068...", // from order details
totalPrice: "1000000", // raw token units
});const result = await client.marketplace.checkoutCart(account, [
{ orderHash: "0x...", considerationToken: "0x033068...", considerationAmount: "1000000" },
{ orderHash: "0x...", considerationToken: "0x033068...", considerationAmount: "500000" },
]);const result = await client.marketplace.cancelOrder(account, {
orderHash: "0x...",
});// Bumps the caller's counter: every previously-registered order becomes unfulfillable.
await client.marketplace.incrementCounter(account);const result = await client.marketplace.mint(account, {
collectionId: "1", // collection ID on the registry
recipient: account.address,
tokenUri: "ipfs://...", // IPFS URI of the metadata JSON
royaltyBps: 500, // EIP-2981 secondary-sale royalty, 0-10_000 (required since MIP v0.4.0)
});const result = await client.marketplace.createCollection(account, {
name: "My Creative Works",
symbol: "MCW",
baseUri: "",
});For IP assets from ERC-1155 collections (e.g. IP-Programmable-ERC1155-Collections). Contract address: read getCoordinates("STARKNET").marketplace1155 from src/chains.ts, the single source of truth across redeploys.
const result = await client.marketplace1155.createListing(account, {
nftContract: "0x...", // ERC-1155 collection address
tokenId: "1",
amount: "10", // number of tokens to sell
pricePerUnit: "1", // human-readable price per token (e.g. "1" USDC)
currency: "USDC",
durationSeconds: 86400 * 30,
});set_approval_for_all is granted automatically if not already in place.
// Fetch order details first to get paymentToken and totalPrice
const details = await client.api.getOrder(orderHash);
const result = await client.marketplace1155.fulfillOrder(account, {
orderHash: "0x...",
paymentToken: "0x033068...", // from order details
totalPrice: "10000000", // pricePerUnit × amount in raw token units
});ERC-2981 royalties are automatically deducted by the contract at fulfillment.
const result = await client.marketplace1155.cancelOrder(account, {
orderHash: "0x...",
});Listing/offer and cancellation are signed; fulfilment is an unsigned call (the buyer is the fulfiller, since v0.26.0): there is no fulfillment typed-data builder.
import { build1155OrderTypedData, build1155CancellationTypedData } from "@medialane/sdk";
import { constants } from "starknet";
const typedData = build1155OrderTypedData(orderParams, constants.StarknetChainId.SN_MAIN);const orders = await client.api.getOrders({
status: "ACTIVE",
sort: "price_asc",
currency: "0x033068...", // USDC address
page: 1,
limit: 20,
});
const order = await client.api.getOrder("0x...");
const tokenOrders = await client.api.getActiveOrdersForToken(contract, tokenId);
const userOrders = await client.api.getOrdersByUser(address);const token = await client.api.getToken(contract, tokenId);
const tokens = await client.api.getTokensByOwner(address);
const history = await client.api.getTokenHistory(contract, tokenId);For ERC-1155 tokens, a single token ID can be held by many wallets simultaneously; read ownership from token.balances:
import type { ApiTokenBalance } from "@medialane/sdk";
const { data: token } = await client.api.getToken(contract, tokenId);
// Check if a wallet owns any quantity of this token
const isOwner = token.balances?.some(
(b: ApiTokenBalance) => b.owner.toLowerCase() === wallet.toLowerCase() && BigInt(b.amount) > 0n
) ?? (token.owner?.toLowerCase() === wallet.toLowerCase());
// How many copies does a wallet hold?
const balance = token.balances?.find((b) => b.owner.toLowerCase() === wallet.toLowerCase());
console.log(`${wallet} holds ${balance?.amount ?? "0"} copies`);
// All current holders
token.balances?.forEach((b: ApiTokenBalance) => {
console.log(`${b.owner}: ${b.amount}`);
});token.owner is deprecated and always null post-migration. token.balances is only populated on single-token fetches (getToken): it is null on list responses.
// All collections: newest first by default
const collections = await client.api.getCollections();
// With sort and pagination
const byVolume = await client.api.getCollections(1, 20, undefined, "volume");
const verified = await client.api.getCollections(1, 18, true, "recent");
// Sort options: "recent" | "supply" | "floor" | "volume" | "name"
const collection = await client.api.getCollection(contract);
const tokens = await client.api.getCollectionTokens(contract);const results = await client.api.search("landscape painting", 10);
// results.data.tokens: matching tokens
// results.data.collections: matching collections
// results.data.creators: matching creator profiles (v0.4.5)const feed = await client.api.getActivities({ type: "sale", page: 1 });
const userFeed = await client.api.getActivitiesByAddress(address);// Upload a file
const fileResult = await client.api.uploadFile(imageFile);
// fileResult.data.url → "ipfs://..."
// Upload metadata JSON
const metaResult = await client.api.uploadMetadata({
name: "My Work",
description: "...",
image: "ipfs://...",
external_url: "https://medialane.io",
attributes: [
{ trait_type: "License", value: "CC BY-NC" },
{ trait_type: "Commercial Use", value: "No" },
// ...
],
});
// metaResult.data.url → "ipfs://..."The intent system handles the SNIP-12 signing flow for marketplace operations:
// 1. Create intent (gets typedData to sign)
const intent = await client.api.createListingIntent({
offerer: address,
nftContract: "0x...",
tokenId: "42",
currency: "0x033068...",
price: "1000000",
endTime: Math.floor(Date.now() / 1000) + 86400 * 30,
});
// 2. Sign typedData
const signature = await account.signMessage(intent.data.typedData);
// 3. Submit signature
await client.api.submitIntentSignature(intent.data.id, toSignatureArray(signature));Mint and collection intents are pre-signed: no signature step needed:
const mintIntent = await client.api.createMintIntent({
owner: ownerAddress,
collectionId: "1",
recipient: recipientAddress,
tokenUri: "ipfs://...",
});
// mintIntent.data.calls → ready to executeimport type { IpAttribute, IpNftMetadata, ApiTokenMetadata } from "@medialane/sdk";
// Single OpenSea ERC-721 attribute
const attr: IpAttribute = { trait_type: "License", value: "CC BY-NC-SA" };
// Full IPFS metadata shape for a Medialane IP NFT
const metadata: IpNftMetadata = {
name: "My Track",
description: "Original music",
image: "ipfs://...",
external_url: "https://medialane.io",
attributes: [
{ trait_type: "IP Type", value: "Audio" },
{ trait_type: "License", value: "CC BY-NC-SA" },
{ trait_type: "Commercial Use", value: "No" },
{ trait_type: "Derivatives", value: "Share-Alike" },
{ trait_type: "Attribution", value: "Required" },
{ trait_type: "Territory", value: "Worldwide" },
{ trait_type: "AI Policy", value: "Not Allowed" },
{ trait_type: "Royalty", value: "10%" },
{ trait_type: "Standard", value: "Berne Convention" },
{ trait_type: "Registration", value: "2026-03-06" },
],
};
// Token from the API: includes indexed licensing fields for fast access
const token = await client.api.getToken(contract, tokenId);
token.data.metadata.licenseType; // "CC BY-NC-SA"
token.data.metadata.commercialUse; // "No"
token.data.metadata.derivatives; // "Share-Alike"
token.data.metadata.attributes; // IpAttribute[] | null| Symbol | Address | Decimals | Listable |
|---|---|---|---|
| USDC | 0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb |
6 | ✓ |
| USDT | 0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8 |
6 | ✓ |
| ETH | 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 |
18 | ✓ |
| STRK | 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d |
18 | ✓ |
| WBTC | 0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac |
8 | ✓ |
import { getTokenBySymbol, getTokenByAddress, getListableTokens, SUPPORTED_TOKENS } from "@medialane/sdk";
const usdc = getTokenBySymbol("USDC");
const token = getTokenByAddress("0x033068...");import {
normalizeAddress, // (chain, address) → canonical form per chain (Starknet pad / EVM EIP-55 / Solana base58)
shortenAddress, // (chain, address) → "0x1234...5678"
getCoordinates, // (chain) → that chain's service coordinates from the registry
CHAINS, // readonly ["STARKNET","ETHEREUM","SOLANA","BASE","BITCOIN"]
type Chain,
parseAmount, // Human-readable → smallest unit BigInt ("1.5", 6) → 1500000n
formatAmount, // Smallest unit → human-readable ("1500000", 6) → "1.5"
stringifyBigInts, // Recursively convert BigInt → string (for JSON)
u256ToBigInt, // u256 { low, high } → BigInt
getListableTokens, // ReadonlyArray<SupportedToken> filtered to listable: true (for dialogs)
} from "@medialane/sdk";import { MedialaneError, MedialaneApiError } from "@medialane/sdk";
// On-chain errors (marketplace module)
try {
await client.marketplace.createListing(account, params);
} catch (err) {
if (err instanceof MedialaneError) {
console.error("On-chain error:", err.message, err.cause);
}
}
// REST API errors
try {
await client.api.getOrders();
} catch (err) {
if (err instanceof MedialaneApiError) {
console.error(`API ${err.status}:`, err.message);
}
}| Option | Type | Default | Description |
|---|---|---|---|
chain |
Chain ("STARKNET" | "ETHEREUM" | "SOLANA" | "BASE" | "BITCOIN") |
"STARKNET" |
The chain this client is scoped to. Coordinates resolve from the coordinates[chain] registry (chains.ts). Replaces network (v0.37.0). |
rpcUrl |
string |
the chain's registry rpcUrl |
JSON-RPC URL override |
backendUrl |
string |
(none) | Medialane API base URL (required for .api.*) |
apiKey |
string |
(none) | API key from Medialane Portal |
marketplace721Contract |
string |
Mainnet default | ERC-721 marketplace protocol override |
marketplaceContract |
string |
Mainnet default | Legacy alias for marketplace721Contract |
marketplace1155Contract |
string |
Mainnet default | ERC-1155 marketplace protocol override |
collection721Contract |
string |
Mainnet default | ERC-721 mint / collection registry override |
collectionContract |
string |
Mainnet default | Legacy alias for collection721Contract |
collection1155Contract |
string |
Mainnet default | ERC-1155 mint / collection factory override |
For integrations that handle signing externally (e.g. a custodial wallet service, Cartridge Controller):
import {
buildOrderTypedData,
buildFulfillmentTypedData,
buildCancellationTypedData,
} from "@medialane/sdk";
const typedData = buildOrderTypedData(orderParams, chainId);
const signature = await account.signMessage(typedData);
await client.api.submitIntentSignature(intentId, signatureArray);bun run build # Compile to dist/ (ESM + CJS dual output)
bun run dev # Watch mode
bun run typecheck # tsc --noEmitBuilt with:
- tsup: dual ESM/CJS bundling
- TypeScript: full type safety
- Zod: runtime config validation
- Peer dep:
starknet >= 6.0.0
Full history in CHANGELOG.md. Highlights below.
- Chain is a first-class axis. New
chains.tscoordinates[chain]registry is the single source of per-chain service coordinates (CHAINS,getCoordinates,DEFAULT_CHAIN,Chain,ChainCoordinates); the flat*_MAINNETconstants derive from it. MedialaneConfig.chainreplacesnetwork: the client is chain-scoped;client.networkgetter →client.chain.ServiceDefinition.onchainis per-chain:Partial<Record<Chain, …>>; readservice.onchain?.STARKNET?.factoryAddress.normalizeAddress(chain, address): per-chain codec (Starknet pad / EVM EIP-55 / Solana base58; Bitcoin not yet implemented).- Removed
SUPPORTED_NETWORKS,DEFAULT_RPC_URL,Network(mainnet-only: coordinates key by chain alone).getChainId(config)throws for non-Starknet.
CollectionRegistryABIexported from@medialane/sdk: minimal ABI coveringlist_user_collectionsandget_collectionon the collection registry contract. Eliminates duplicated inline ABI definitions in consuming apps.
COLLECTION_CONTRACT_MAINNETupdated to audited v2 contract address0x05c49ee5d3208a2c2e150fdd0c247d1195ed9ab54fa2d5dea7a633f39e4b205b
- ERC-1155 support:
ApiToken.balances: ApiTokenBalance[] | nullreplaces the singleownerfield for ownership checks ApiTokenBalancetype:{ owner: string; amount: string }: each entry represents one holder and their quantityApiToken.ownerdeprecated: alwaysnullafter the ERC-1155 migration; usebalancesinsteadApiCollection.standard:"ERC721" | "ERC1155" | "UNKNOWN"detected via ERC-165supportsInterfacetotalSupplyfix: ERC-1155 collections now reportSUM(holder amounts)for an accurate circulating total
- Collection Drop: new
DropService(client.services.drop) with full on-chain drop management:claim,adminMint,setClaimConditions,setAllowlistEnabled,addToAllowlist,batchAddToAllowlist,setPaused,withdrawPayments,createDrop client.api.getDropCollections(opts?): list allCOLLECTION_DROPcollectionsclient.api.getDropMintStatus(collection, wallet): returns{ mintedByWallet, totalMinted }DropMintStatus,ClaimConditions,CreateDropParamstypes exportedDropCollectionABIandDropFactoryABIexported from@medialane/sdkDROP_FACTORY_CONTRACT_MAINNETandDROP_COLLECTION_CLASS_HASH_MAINNETconstants exportedCollectionSourceunion extended with"COLLECTION_DROP"
- POP Protocol:
PopService(client.services.pop):claim,adminMint,addToAllowlist,batchAddToAllowlist,removeFromAllowlist,setTokenUri,setPaused,createCollection client.api.getPopCollections(opts?)andclient.api.getPopEligibility(collection, wallet)POPCollectionABIandPOPFactoryABIexportedPOP_FACTORY_CONTRACT_MAINNETandPOP_COLLECTION_CLASS_HASH_MAINNETconstants exported
ApiCollectionProfile.hasGatedContent: boolean: whether the collection has token-gated content configuredApiCollectionProfile.gatedContentTitle: string | null: public title of gated content (shown to all users; URL is accessible to holders only via the backend gated-content endpoint)
extendRemixOffer(id, days, siwsToken): requester extends expiry of a PENDING/AUTO_PENDING remix offer by 1–30 days (POST /v1/remix-offers/:id/extend)ApiRemixOfferPricetype:{ raw, formatted, currency, decimals }replaces flatproposedPrice/proposedCurrencyfields onApiRemixOffer.price(visible to participants only)
ApiRemixOffer.priceshape introduced: backend now serializes price as a structured object (raw,formatted,currency,decimals), replacing raw wei strings
getTokenComments(contract, tokenId, opts?): fetch on-chain NFT comments for a token (GET /v1/tokens/:contract/:tokenId/comments)ApiCommenttype:{ id, author, content, txHash, blockNumber, blockTimestamp, isHidden, createdAt }
- Counter-offer support:
createCounterOfferIntent(params, siwsToken),getCounterOffers(query),ApiCounterOffersQuery,CreateCounterOfferIntentParams OrderStatusextended with"COUNTER_OFFERED";IntentTypewith"COUNTER_OFFER"ApiOrderextended:parentOrderHash?: string | null,counterOfferMessage?: string | null- Remix licensing: full set of remix offer methods and types:
submitRemixOffer(params, siwsToken): custom offersubmitAutoRemixOffer(params, siwsToken): auto offer for open-license tokensconfirmSelfRemix(params, siwsToken): record owner self-remixgetRemixOffers(query, siwsToken): list by rolegetRemixOffer(id, siwsToken?): single offerconfirmRemixOffer(id, params, siwsToken): creator approvesrejectRemixOffer(id, siwsToken): creator rejectsgetTokenRemixes(contract, tokenId, opts?): public remix list
- New types:
RemixOfferStatus,ApiRemixOffer,ApiPublicRemix,OPEN_LICENSES,OpenLicense,CreateRemixOfferParams,AutoRemixOfferParams,ConfirmSelfRemixParams,ConfirmRemixOfferParams,ApiRemixOffersQuery
ApiCommenttype +getTokenComments(patch release, backported into v0.5.3)
IPTypeunion type exported:"Audio" | "Art" | "Documents" | "NFT" | "Video" | "Photography" | "Patents" | "Posts" | "Publications" | "RWA" | "Software" | "Custom"
ApiUserWallettype +upsertMyWallet(siwsToken)/getMyWallet(siwsToken)for wallet registration fallback (POST/GET /v1/users/me)
ApiSearchCreatorResulttype +ApiSearchResult.creators: creator profiles now included in search results
ApiCreatorListResult+getCreators(opts?): list creators with search/pagination viaGET /v1/creators
ApiCreatorProfile.usernamefield +getCreatorByUsername(username): resolve username slug to creator profile
- WBTC added to
SUPPORTED_TOKENS(0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac, 8 decimals) listablefield on everySUPPORTED_TOKENSentry: controls whether a token appears in listing/offer dialogs vs filter-onlygetListableTokens(): returns tokens filtered tolistable: true; exported from package root- ETH promoted to
listable: true: now available in listing and offer dialogs - USDC.e removed: bridged USDC (
0x053c91...) removed entirely; only Circle-native USDC remains, to avoid user confusion
- Collection claims:
claimCollection(contractAddress, walletAddress, siwsToken)for on-chain ownership verification;requestCollectionClaim({ contractAddress, walletAddress?, email, notes? })for manual review - Collection profiles:
getCollectionProfile(contractAddress)andupdateCollectionProfile(contractAddress, data, siwsToken)for enriched display metadata (displayName, description, image, bannerImage, social links) - Creator profiles:
getCreatorProfile(walletAddress)andupdateCreatorProfile(walletAddress, data, siwsToken)for creator display metadata - New types:
ApiCollectionClaim,ApiAdminCollectionClaim,ApiCollectionProfile,ApiCreatorProfile ApiCollectionextended withsource("MEDIALANE_REGISTRY" | "EXTERNAL" | "PARTNERSHIP" | "IP_TICKET" | "IP_CLUB" | "GAME") andclaimedBy: string | nullprofile?: ApiCollectionProfile | nulloptionally embedded onApiCollectionwhen?include=profile
- Typed error codes:
MedialaneErrorandMedialaneApiErrornow expose a.code: MedialaneErrorCodeproperty ("TOKEN_NOT_FOUND"|"RATE_LIMITED"|"INTENT_EXPIRED"|"UNAUTHORIZED"|"INVALID_PARAMS"|"NETWORK_NOT_SUPPORTED"|"UNKNOWN") - Automatic retry: all API requests retry up to 3 times with exponential backoff (300ms base, 5s cap) on transient failures. Configure via
retryOptionsinMedialaneConfig RetryOptionstype exported from indexCollectionSortnamed union type exported ("recent" | "supply" | "floor" | "volume" | "name")- Sepolia guard: constructing a client with
network: "sepolia"and no explicit contract addresses now throwsNETWORK_NOT_SUPPORTEDimmediately
getCollections(page?, limit?, isKnown?, sort?): addedsortparameter:"recent"(default) |"supply"|"floor"|"volume"|"name"- Default sort changed from
totalSupply DESCtocreatedAt DESC(newest first): matches backend default
ApiCollection.collectionId: string | null: on-chain registry numeric ID (decimal string). Required forcreateMintIntent. Populated for collections indexed after 2026-03-09.
normalizeAddress()applied internally before all API calls: callers no longer need to normalize Starknet addressesApiCollection.owner: string | null: populated from intent typedData or on-chainowner()callgetCollectionsByOwner(owner): fetch collections by wallet address viaGET /v1/collections?owner=
ApiOrder.token: ApiOrderTokenMeta | null: token name/image/description embedded on orders (batchTokenMeta); no per-rowgetTokencalls needed
IpAttributeandIpNftMetadatainterfaces for IP metadataApiTokenMetadata.attributestyped asIpAttribute[] | null(wasunknown)ApiTokenMetadataextended withderivatives,attribution,territory,aiPolicy,royalty,registration,standard- Added
USDC.e(bridged USDC via Starkgate) toSUPPORTED_TOKENS
- Initial release: orders, tokens, collections, activities, intents, metadata, portal
- Marketplace: medialane.io
- Starknet App: starknet.medialane.io
- Developer Portal: portal.medialane.io
- npm: npmjs.com/package/@medialane/sdk
- GitHub: github.com/medialane-io