@goodz-core/sdk
v0.27.9
Published
Official SDK for the GoodZ Ecosystem — unified API client for Core, Commerce, Exchange, Alive, and Shops. Includes OAuth helpers, Z-coin utilities, and React UI components.
Downloads
464
Maintainers
Readme
@goodz-core/sdk
Official SDK for the GoodZ ecosystem — a unified, type-safe API client for building GoodZ-powered applications. One package covers all five products: Core, Commerce, Exchange, Alive, and Shops. Stripe-style namespaces route requests to the correct service transparently.
Installation
npm install @goodz-core/sdk
# or
pnpm add @goodz-core/sdkRuntime dependency: superjson only. Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and browsers (any runtime with Fetch API).
Prerequisites: Get Your Credentials
Before calling any authenticated API, you need a GoodZ OAuth App (clientId + clientSecret) and a user access token. There are two ways to get set up — choose based on who is doing the integration:
| Path | Who | How to register | How to get token |
|------|-----|----------------|------------------|
| AI Agent | An MCP-connected Agent (Claude, GPT, etc.) | Call register_app() on Core MCP → credentials returned in response | Call get_auth_url() → user logs in → token returned |
| Human Developer | A person writing code | Go to Developer Console → Register App → copy credentials | Implement OAuth flow with buildAuthorizationUrl() + exchangeCode() |
Agent path (zero-dashboard):
# Connect to GoodZ Core MCP
MCP endpoint: https://goodzcore.manus.space/api/mcp
# Step 1: Register an app (no auth needed to call this)
register_app({ app_name: "My App", redirect_uris: ["https://myapp.com/callback"] })
→ { client_id: "od_xxxxxxxx", client_secret: "gk_live_..." } # Save these!
# Step 2: Get user token
get_auth_url()
→ { login_url: "https://..." } # User clicks this, logs in, gets tokenHuman path (traditional):
- Visit
https://goodzcore.manus.space/dev-huband log in - Click "Register App" → fill in name and redirect URIs
- Copy
clientIdandclientSecret(secret shown once!) - Implement the OAuth flow in your app (see Authentication below)
Don't need user auth? For read-only public data (catalog, card details, instance profiles), no credentials are needed — just call
createGoodZClient()without a token.
Quick Start
import { createGoodZClient } from "@goodz-core/sdk";
const goodz = createGoodZClient({
accessToken: "your-jwt-token",
});
// Core — settlement
const balance = await goodz.zcoin.getMyBalance();
const trade = await goodz.zcoin.commercialTransfer({
instanceId: 90447,
buyerUserId: 1,
sellerUserId: 2,
priceZcoin: 1050, // 10.50 Z-coin in hundredths
saleType: "direct",
referenceId: "order-abc-123",
appClientId: "od_myapp",
});
// Commerce — register a merchant and launch a campaign
const merchant = await goodz.commerce.getOrCreateMerchant({ name: "Frostpeak Store" });
const campaign = await goodz.commerce.launchCampaign({ merchantId: merchant.id, name: "Spring Drop" });
const order = await goodz.commerce.executePurchase({ campaignId: campaign.id, quantity: 1 });
// Exchange — list an item for sale
const listing = await goodz.exchange.createListing({
instanceId: 42,
listingType: "fixed_price",
askPrice: 2000,
});
const data = await goodz.exchange.getMarketData({ coreGoodzId: 42 });
// Alive — chat with a character
const reply = await goodz.alive.sendMessage({
instanceId: 1,
userId: 1,
message: "What's your favorite memory?",
});
// Shops — create and publish a storefront
const themes = await goodz.shops.listThemes({ mood: "warm" });
const storefront = await goodz.shops.createStorefront({
slug: "frostpeak-official",
name: "Frostpeak Official Store",
merchantId: merchant.id,
config: { version: 1, meta: {}, theme: themes[0].theme, merchantId: merchant.id, sections: [] },
});
await goodz.shops.publishStorefront({ id: storefront.id });Architecture
The SDK is built on three principles:
One client, all services. createGoodZClient() returns a single object with namespaces for every GoodZ product. Core uses tRPC HTTP wire protocol; sub-sites (Commerce, Exchange, Alive) use MCP JSON-RPC 2.0. The client handles routing transparently — developers never need to know which protocol a method uses.
Hand-crafted types as API contract. All input/output types are defined inside the SDK itself, mirroring the Zod schemas on Core and the MCP tool schemas on sub-sites. This is the same approach used by the Stripe SDK — types serve as both documentation and compile-time contract. When any service's API evolves, the SDK types are updated and a new version is published.
Zero tRPC dependency. The SDK speaks the tRPC wire protocol directly using fetch + superjson, and speaks MCP JSON-RPC natively. Consumers never need to install @trpc/client, @trpc/server, or any MCP client library.
Z-coin Unit Convention
All monetary values in the GoodZ ecosystem use Z-coin hundredths as the internal unit:
| Internal Value | Display Value | Meaning |
|---------------|---------------|----------|
| 100 | "1.00" | 1 Z-coin |
| 1050 | "10.50" | 10.50 Z-coin |
| 50 | "0.50" | 0.50 Z-coin |
Key rules:
- All
*Centsfields are Z-coin hundredths, NOT fiat cents. TheCentssuffix is a legacy naming convention from Commerce.retailPriceCents: 1050means 10.50 Z-coin, not $10.50 USD. - All
*Zcoinfields are also hundredths.priceZcoin: 2000means 20.00 Z-coin. - Use
*_displayfields for UI. Balance responses include bothbalance_hundredths(for math) andbalance_display(for rendering). get_deposit_urlis the exception — itsamountparameter uses display units (100 = 100 Z-coin, not 1 Z-coin).
Conversion helper:
import { toDisplay, toHundredths, formatZcoin } from "@goodz-core/sdk/zcoin-utils";
toDisplay(1050); // 10.5
formatZcoin(1050); // "10.50"
toHundredths(10.50); // 1050Tip for Agent developers: When calling MCP tools, always check the tool description for the unit. Most tools use hundredths, but
get_deposit_urluses display units.
Client Configuration
interface GoodZClientConfig {
coreUrl?: string; // Default: "https://goodzcore.manus.space"
commerceUrl?: string; // Default: "https://goodz-commerce.manus.space"
exchangeUrl?: string; // Default: "https://goodz-exchange.manus.space"
aliveUrl?: string; // Default: "https://goodz-alive.manus.space"
shopsUrl?: string; // Default: "https://goodz-shops.manus.space"
accessToken?: string; // Static JWT token
getAccessToken?: () => string | Promise<string>; // Dynamic token provider
headers?: Record<string, string>; // Custom headers for every request
}The getAccessToken function takes precedence over accessToken when both are provided. Use it for auto-refresh scenarios. Sub-site URLs default to production endpoints and rarely need to be changed.
API Reference — Core Namespaces
Core namespaces communicate via tRPC HTTP wire protocol with goodzcore.manus.space.
goodz.zcoin — Z-coin Payment & Settlement
| Method | Type | Description |
|--------|------|-------------|
| getMyBalance() | Query | Authenticated user's Z-coin balance |
| getMyHistory(input?) | Query | Transaction history with pagination |
| getDepositPackages(input?) | Query | Available deposit packages with pricing |
| createDepositOrder(input) | Mutation | Create Stripe checkout for Z-coin deposit |
| getDepositStatus(input) | Query | Check deposit checkout session status |
| getDepositUrl(input?) | Query | Generate a deposit deep link URL for third-party app redirects |
| commercialTransfer(input) | Mutation | Primary API for secondary market. Atomic: debit buyer → credit seller → transfer ownership |
| mintAndCharge(input) | Mutation | Primary API for primary sales. Atomic: mint instance → charge buyer |
| chargeUser(input) | Mutation | Charge Z-coin for in-app purchases |
| createDirectPurchaseOrder(input) | Mutation | Fiat-to-Z-coin direct purchase checkout |
commercialTransfer is the most important method in the SDK. It atomically debits the buyer's Z-coin balance, credits the seller (minus platform fee), and transfers card instance ownership — all in a single database transaction. The referenceId parameter provides idempotency.
mintAndCharge is the primary API for primary sales (gacha, direct-from-creator). It mints a new card instance and charges the buyer in one atomic operation. Requires prior mint authorization via inventory.grantMintAuth.
getDepositUrl generates a deep link URL that redirects users to Core's deposit page. After completing the Stripe checkout, the user is redirected back to your app via returnUrl. This is the recommended way for third-party apps to handle "insufficient balance" scenarios:
// When user's balance is too low, redirect them to deposit
const { url } = await goodz.zcoin.getDepositUrl({
amount: 100, // suggested amount
returnUrl: "https://myapp.com/shop", // return here after deposit
appId: "od_myapp", // your app's client_id
});
window.open(url, "_blank");goodz.inventory — Card Instance Management
| Method | Type | Description |
|--------|------|-------------|
| getMyInventory(input?) | Query | Authenticated user's own inventory. Supports seriesId, franchiseId, rarity, formFactor filters + sortBy/sortOrder sorting + cursor-based pagination. Returns { items, nextCursor, hasMore } |
| getUserInventory(input) | Query | User's owned card instances. Same filters, sorting, and cursor pagination as above |
| getStats(input?) | Query | Inventory summary statistics (total instances, franchises, series) with franchise→series breakdown. Supports franchiseId, rarity, formFactor filters |
| confirmOwnership(input) | Query | Check if user owns a specific card |
| mint(input) | Mutation | Mint new instances (requires franchise ownership) |
| batchMint(input) | Mutation | Batch mint multiple cards in one call (max 100 total instances). Requires franchise ownership |
| mintFor(input) | Mutation | Authorized party mints — claimed (toUserId) or unclaimed (claim tokens). Requires mint authorization |
| mintAsGift(input) | Mutation | Mint as gift — creates unclaimed instances with claim URLs for distribution. Requires franchise ownership |
| transfer(input) | Mutation | Transfer instance by instanceId |
| transferByCard(input) | Mutation | Transfer by cardId (oldest first) |
| grantMintAuth(input) | Mutation | Grant mint authorization to app/user |
| revokeMintAuth(input) | Mutation | Revoke a mint authorization. Cannot revoke if locked by an APP |
| listMintAuths(input) | Query | List mint authorizations filtered by cardId, granteeUserId, or status |
| transferHistory(input) | Query | Ownership/transfer history |
For commercial transactions, always use zcoin.commercialTransfer or zcoin.mintAndCharge instead of raw transfer/mint. The raw methods are for administrative operations and gift transfers only.
goodz.collectible — Card & Instance Queries
| Method | Type | Description |
|--------|------|-------------|
| getInstanceById(input) | Query | Instance by numeric ID |
| getPublicInstance(input) | Query | Instance by instance code |
| getPublicInstancesBatch(input) | Query | Batch-fetch instances (max 100) |
| getCardProfile(input) | Query | Card metadata, rarity, series info |
| getShellImageUrl(input) | Query | Shell (packaging) image URL |
goodz.ip — IP Management (Franchise/Series/Card)
| Method | Type | Description |
|--------|------|-------------|
| listFranchises(input?) | Query | List all published franchises with optional ownerId, limit, offset filters. No auth required |
| getFranchise(input) | Query | Franchise by ID or slug |
| getSeries(input) | Query | Series by ID or slug |
| listSeriesByFranchise(input) | Query | All series in a franchise |
| getCard(input) | Query | Card by ID |
| listCardsBySeries(input) | Query | All cards in a series |
| listMyFranchises() | Query | Franchises owned by authenticated user (requires auth) |
goodz.platform — App/Merchant Public Info
| Method | Type | Description |
|--------|------|-------------|
| getAppInfo(input) | Query | Get public info about a registered app/merchant by clientId. Returns name, description, icon, website URL. No auth required |
// Get shop branding for your independent site
const appInfo = await goodz.platform.getAppInfo({ clientId: "od_mymerchant" });
console.log(appInfo.name, appInfo.iconUrl);goodz.user & goodz.auth
| Method | Type | Description |
|--------|------|-------------|
| user.getPublicProfile(input) | Query | Profile by openId |
| user.getPublicProfileById(input) | Query | Profile by userId |
| auth.me() | Query | Authenticated user's profile |
| auth.getOAuthAppInfo(input) | Query | OAuth app info by clientId |
goodz.settlement — Creator Revenue & Payouts
| Method | Type | Description |
|--------|------|-------------|
| getMyBalance() | Query | Settlement credit balance — available balance, lifetime totals, claimed Z-coin, fiat payouts |
| getMyHistory(input?) | Query | Transaction history — paginated list of credit/claim/payout events with limit, offset |
| claimAsZcoin(input) | Mutation | Claim credits as Z-coin — converts settlement credits to spendable Z-coin. Atomic & idempotent |
Settlement credits ≠ Z-coin. When a buyer purchases GoodZ, the seller’s proceeds accumulate as settlement credits (not direct Z-coin). Sellers can claim credits as Z-coin via
claimAsZcoin()or (future) request fiat payout.
// Check creator earnings
const balance = await goodz.settlement.getMyBalance();
console.log(`Available: ${balance.balanceDisplay} Z-coin`);
console.log(`Lifetime earned: ${balance.totalCreditedDisplay}`);
// Claim 1000 Z-coin hundredths (10.00 Z-coin display)
const result = await goodz.settlement.claimAsZcoin({ amount: 1000 });
console.log(`New Z-coin balance: ${result.newZcoinBalanceDisplay}`);
// View transaction history
const history = await goodz.settlement.getMyHistory({ limit: 20 });
for (const tx of history.transactions) {
console.log(`${tx.txType}: ${tx.amountDisplay} at ${new Date(tx.occurredAt).toISOString()}`);
}goodz.burn (Destructive Operations)
| Method | Type | Description |
|--------|------|-------------|
| request(input) | Mutation | Request to burn instances. Returns consent URL for user confirmation |
| confirm(input) | Mutation | Confirm burn (called from consent page). Irreversible |
| cancel(input) | Mutation | Cancel a pending burn request |
| getRequest(input) | Query | Get burn request status |
| getReceipt(input) | Query | Get burn receipt (after confirmation) |
Security: Burn is a two-step process.
request()creates a pending request and returns aconsentUrl. The user MUST be redirected to this URL to confirm.confirm()is only callable from the consent page. This prevents accidental or unauthorized destruction of collectibles.
// Step 1: App requests burn
const { consentUrl } = await goodz.burn.request({
instanceIds: [101, 102],
reason: "Recycle for points",
callbackUrl: "https://myapp.com/burn/callback",
});
// Step 2: Redirect user to consent page
window.location.href = consentUrl;
// Step 3: After user confirms, check receipt
const receipt = await goodz.burn.getReceipt({ requestId: "abc123..." });API Reference — Commerce Namespace
Commerce methods communicate via MCP JSON-RPC with goodz-commerce.manus.space (46 tools). This namespace covers merchant management, product assembly, campaigns, purchases, wholesale, redemption, and settlement.
Merchant & Product Assembly
| Method | Description |
|--------|-------------|
| getOrCreateMerchant(input?) | Get or create a merchant (idempotent) |
| getMerchantDashboard() | Merchant dashboard with stats |
| createBatch(input) | Create a product batch (fixed-price bundle) |
| createGachaPool(input) | Create a gacha/blind-box pool with probability weights |
Campaign Lifecycle
| Method | Description |
|--------|-------------|
| launchCampaign(input) | Launch a new sales campaign |
| activateCampaign(input) | Activate a paused campaign |
| pauseCampaign(input) | Pause an active campaign |
| endCampaign(input) | End a campaign permanently |
| updateCampaign(input) | Update campaign details |
| getCampaignInfo(input) | Get campaign info |
| getCampaignItems(input) | Get items in a campaign |
Purchase & Orders
| Method | Description |
|--------|-------------|
| executePurchase(input) | Execute a purchase (direct or gacha draw) |
| checkOrderStatus(input) | Check order/checkout/batch status by orderId, checkoutSessionId, or batchSessionId. Useful for anonymous purchases |
| getMyOrders(input?) | User's order history (paginated: page, page_size) |
| getOrderDetail(input) | Get order detail by order_id or order_no |
| refundOrder(input) | Refund a confirmed/paid order (merchant owner only) |
| cancelOrder(input) | Cancel a pending order (merchant owner only) |
Wholesale
| Method | Description |
|--------|-------------|
| publishToWholesale(input) | Publish batch to wholesale market |
| browseWholesale() | Browse wholesale listings |
| procureBatch(input) | Procure a wholesale batch for resale |
Inventory & Fulfillment
| Method | Description |
|--------|-------------|
| manageInventory(input) | View/manage merchant inventory |
| registerInstances(input) | Register minted instances to merchant |
| mintToInventory(input) | Mint + register in one step |
| requestRedemption(input) | Request physical redemption for an instance |
| approveRedemption(input) | Approve a redemption request (merchant owner) |
| rejectRedemption(input) | Reject a redemption request (merchant owner) |
| shipRedemption(input) | Mark redemption as shipped with tracking info |
| confirmRedemptionReceived(input) | Confirm redemption received (buyer) |
| cancelRedemption(input) | Cancel a pending redemption |
| getRedemptionStatus(input) | Get redemption request status |
| getMyRedemptions(input?) | List user's redemption requests |
Wallet / Balance
| Method | Description |
|--------|-------------|
| getMyBalance(input?) | Query user's real Z-coin balance from Core (single source of truth) |
| topUpWallet(input) | Top up Z-coin wallet via Stripe |
Settlement & Discovery
| Method | Description |
|--------|-------------|
| getSettlementReport() | Revenue and settlement report |
| searchMarketplace(input) | Search across all shops |
| getShopsByBlueprint(input) | Find shops selling a specific card |
| getMerchantownerSkill(input?) | Get merchant owner onboarding guide |
Webhooks & App Management
| Method | Description |
|--------|-------------|
| registerWebhook(input) | Register event webhook |
| listWebhooks(input?) | List registered webhooks |
| testWebhook(input) | Test a webhook endpoint |
| deleteWebhook(input) | Delete a webhook |
| registerApp(input) | Register an app on Commerce |
| updateApp(input) | Update app details |
| listMyApps() | List your registered apps |
| getAuthUrl() | Get Commerce OAuth URL |
| learnMerchant(input?) | Get Commerce platform documentation (self-describing) |
Commerce Escape Hatch
// For MCP tools not yet in the typed API
const result = await goodz.commerce.rawTool("some_new_tool", { key: "value" });API Reference — Exchange Namespace
Exchange methods communicate via MCP JSON-RPC with goodz-exchange.manus.space (23 tools). This namespace covers the secondary market: listings, auctions, want-to-buy requests, P2P trades, watchlist, and market data.
Marketplace & Listings
| Method | Description |
|--------|-------------|
| browseMarketplace(input?) | Browse active listings with filters (type, GoodZ, condition, price, sort) |
| getListingDetail(input) | Get listing details |
| createListing(input) | Create listing (fixed_price or auction). Core locks the instance |
| cancelListing(input) | Cancel listing. Core unlocks the instance |
| getMyListings(input?) | Get your own listings (active, sold, cancelled, expired) |
Purchase & Bidding & Orders
| Method | Description |
|--------|-------------|
| buyListing(input) | Buy a fixed-price listing. Z-coin debit + ownership transfer |
| placeBid(input) | Bid on auction. If meets buy-now price → instant purchase |
| getBids(input) | All bids for an auction (highest first) |
| getMyOrders(input?) | Your order history (as buyer and/or seller, filterable by role) |
Want-to-Buy (WTB)
| Method | Description |
|--------|-------------|
| createWtb(input) | Post a want-to-buy request |
| browseWtb(input?) | Browse active WTB requests (filter by GoodZ, franchise, series, condition, price) |
| fulfillWtb(input) | Fulfill a WTB by offering your item |
| cancelWtb(input) | Cancel your WTB request |
P2P Trade
| Method | Description |
|--------|-------------|
| proposeTrade(input) | Propose an item swap (optional Z-coin compensation) |
| respondToTrade(input) | Accept/reject trade. If accepted → all items transfer atomically |
Watchlist & Market Data
| Method | Description |
|--------|-------------|
| getWatchlist(input?) | Your watchlist (paginated) |
| addToWatchlist(input) | Add to watchlist |
| removeFromWatchlist(input) | Remove from watchlist |
| getMarketData(input) | Price history, floor price, volume, trends. Supports period for aggregation |
| getCollectionStats(input) | Collection stats: floor price, listings, 24h/7d volume, avg price |
Batch Lookup & User Profile
| Method | Description |
|--------|-------------|
| getInstancesBatch(input) | Batch lookup card instances by IDs (max 100) with full metadata |
| getUserProfile(input) | Get a user's public profile (displayName, avatar, role) |
| learnExchange(input?) | Get Exchange platform documentation |
Exchange Escape Hatch
const result = await goodz.exchange.rawTool("some_new_tool", { key: "value" });API Reference — Alive Namespace
Alive methods communicate via MCP JSON-RPC with goodz-alive.manus.space (9 tools). This namespace powers AI companion interactions: memory, intimacy, context building, and conversation.
| Method | Description |
|--------|-------------|
| storeMemory(input) | Store a memory for a character-user pair |
| recallMemories(input) | Semantic search for relevant memories |
| forgetMemory(input) | Delete a specific memory |
| getIntimacy(input) | Get intimacy/affection state between character and user |
| updateIntimacy(input) | Update intimacy level after interaction |
| buildContext(input) | Build full AI context (profile + memories + history + system prompt) |
| classifyIntent(input) | Classify user message into intent category |
| sendMessage(input) | Send message → get character response (includes memory/intimacy processing) |
| learnAlive(input?) | Get Alive platform documentation |
Alive Escape Hatch
const result = await goodz.alive.rawTool("some_new_tool", { key: "value" });API Reference — Shops Namespace
Shops methods communicate via MCP JSON-RPC with goodz-shops.manus.space. This namespace manages storefront creation, WCAG-validated themes, asset uploads, and settlement credit management.
Key types: ShopsStorefrontTheme (11 typed properties), ShopsSection (discriminated union of 13 section types), ShopsStorefrontMeta (SEO metadata), ShopsStorefrontConfig (full config). See types-shops.ts for complete definitions.
Storefront CRUD
| Method | Description |
|--------|-------------|
| createStorefront(input) | Create a new fully-managed storefront from a StorefrontConfig JSON. Returns storefront ID and preview URL |
| getStorefront(input) | Get the full StorefrontConfig for a storefront by ID or slug |
| updateStorefront(input) | Update a storefront's configuration (partial updates supported) |
| publishStorefront(input) | Publish a storefront, making the current draft version live |
| previewStorefront(input) | Get the preview URL for a storefront (works for draft and published) |
Themes
| Method | Description |
|--------|-------------|
| listThemes(input?) | List all 12 WCAG AA-validated theme presets across 6 moods |
| generateTheme(input) | Generate a custom StorefrontTheme using AI from a text description |
Assets & Documentation
| Method | Description |
|--------|-------------|
| uploadAsset(input) | Upload an image/asset to the Shops CDN (base64 or URL) |
| learnShops(input?) | Get platform documentation about GoodZ.Shops |
Settlement Credits
| Method | Description |
|--------|-------------|
| getSettlementBalance() | Get current claimable balance, lifetime earnings, and lifetime claimed |
| getSettlementHistory(input?) | Get paginated settlement credit transaction history |
| claimSettlementAsZcoin(input) | Claim settlement credits as Z-coin (⚠️ financial operation) |
Shop Branding
| Method | Description |
|--------|-------------|
| getShopBranding(input) | Get branding info (logo, banner, colors) for a single shop |
| batchShopBranding(input) | Batch get branding info for multiple shops (max 50) |
All settlement amounts are in Z-coin hundredths (divide by 100 for display, e.g., 100 = 1.00 Z-coin).
Section Types (13 types)
Storefronts are built from modular sections. The ShopsSection union type covers:
hero · story · campaigns · collection · rarity · features · gallery · testimonials · faq · newsletter · cta · footer · custom_html
Each section type has a dedicated interface (e.g., ShopsHeroSection, ShopsCampaignsSection) with typed properties.
Authentication
Where do I get
clientId? See Prerequisites above. AI Agents callregister_app()via MCP; human developers register at the Developer Console.
Static Token (simplest)
const goodz = createGoodZClient({
accessToken: process.env.CORE_ACCESS_TOKEN,
});Auto-Refresh with TokenManager (recommended)
For long-running services that need automatic token refresh. Pass tokenManager directly — the SDK handles everything:
- Pre-request: calls
tokenManager.getValidToken()to get a fresh token (proactive refresh before expiry) - On 401: force-refreshes the token and retries the request once (handles clock skew / server-side revocation)
- Concurrent dedup: multiple simultaneous 401s trigger only one refresh
- Callback:
onTokenRefreshfires after every successful refresh so you can persist new tokens
import { createGoodZClient } from "@goodz-core/sdk/core";
import { TokenManager } from "@goodz-core/sdk/auth";
const tokenManager = new TokenManager({
clientId: "od_myapp",
initialTokens: {
accessToken: savedAccessToken,
refreshToken: savedRefreshToken,
expiresAt: savedExpiresAt,
},
onTokenRefresh: async (tokens) => {
// Persist rotated tokens to your database
await db.update(appTokens).set({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
});
},
});
// v0.4.0+: pass tokenManager directly — auto-refresh on 401 is built in
const goodz = createGoodZClient({ tokenManager });
// All calls are now fully transparent — no manual token handling needed
const balance = await goodz.zcoin.getMyBalance(); // Core (tRPC)
const dashboard = await goodz.commerce.getMerchantDashboard(); // Commerce (MCP)
const market = await goodz.exchange.browseMarketplace(); // Exchange (MCP)
const memory = await goodz.alive.recallMemories(...); // Alive (MCP)Migration from v0.3.x: Replace
{ getAccessToken: () => tokenManager.getValidToken() }with{ tokenManager }. The old pattern still works but does not get 401 auto-retry.v0.4.1: Auto-refresh now covers all transports — Core (tRPC), Commerce, Exchange, and Alive (MCP). The shared dedup ensures concurrent 401s across different transports trigger only one refresh.
Dynamic Token Provider (legacy)
If you manage tokens yourself without TokenManager:
const goodz = createGoodZClient({
getAccessToken: async () => {
const token = await myCustomRefreshLogic();
return token;
},
});Note: This pattern does NOT get automatic 401 retry. Use
tokenManagerfor full auto-refresh.
OAuth Authorization Code Flow with PKCE S256
GoodZ.Core requires PKCE S256 (RFC 7636) for all authorization requests. The SDK provides helpers to generate the code verifier and compute the S256 challenge automatically.
import {
buildAuthorizationUrl,
exchangeCode,
generateCodeVerifier,
} from "@goodz-core/sdk/auth";
// Step 1: Generate PKCE code verifier (store it for the token exchange)
const codeVerifier = generateCodeVerifier();
// Step 2: Build authorization URL (async — computes S256 challenge)
const authUrl = await buildAuthorizationUrl({
clientId: "od_myapp",
redirectUri: "https://myapp.com/callback",
scope: "read write",
state: crypto.randomUUID(),
codeVerifier, // SDK auto-computes BASE64URL(SHA256(verifier))
});
// Step 3: Redirect user to authUrl
// ... user authenticates and is redirected back with ?code=xxx&state=xxx
// Step 4: Exchange code for tokens (pass the same verifier)
const tokens = await exchangeCode({
clientId: "od_myapp",
clientSecret: process.env.CLIENT_SECRET, // optional for confidential clients
code: searchParams.get("code"),
redirectUri: "https://myapp.com/callback",
codeVerifier, // MUST match the verifier used in Step 2
});
// Step 5: Create a user-scoped client
const userGoodz = createGoodZClient({
accessToken: tokens.accessToken,
});PKCE S256 is mandatory. GoodZ.Core rejects
code_challenge_method=plain. ThebuildAuthorizationUrl()function is async because it uses Web Crypto's SHA-256 digest. It works in Node.js 18+, Deno, Bun, Cloudflare Workers, and all modern browsers.
Token Revocation
Revoke a refresh token when the user logs out (RFC 7009):
import { revokeToken } from "@goodz-core/sdk/auth";
// Standalone function
await revokeToken(storedRefreshToken);
// Or via TokenManager
await tokenManager.revokeToken();Browser-Side Auth (SPA / Vanilla JS)
New in v0.13.0 —
@goodz-core/sdk/browser-auth
For single-page apps or vanilla JS projects that need browser-side OAuth without React:
import { GoodZBrowserAuth } from "@goodz-core/sdk/browser-auth";
// Step 1: Trigger login (redirects to GoodZ login page)
await GoodZBrowserAuth.startLoginAndRedirect({
clientId: "od_myapp",
coreBaseUrl: "https://goodzcore.manus.space",
});
// Step 2: Handle callback (call once at app startup)
const handled = await GoodZBrowserAuth.handleCallback({
clientId: "od_myapp",
onSuccess: (tokens) => {
// tokens.access_token, tokens.refresh_token, tokens.open_id
localStorage.setItem("goodz_token", tokens.access_token);
},
onError: (err) => console.error("Login failed:", err.message),
});How it works:
startLoginAndRedirect()generates a PKCE S256 challenge, stores the verifier insessionStorage, and redirects to Core's authorization endpoint.handleCallback()detects theauth_codeURL parameter, retrieves the stored verifier, exchanges the code for tokens, and cleans the URL.- Uses Web Crypto API — works in all modern browsers, no Node.js dependencies.
Also available: getAppToken() for M2M client_credentials grants:
import { getAppToken } from "@goodz-core/sdk/browser-auth";
const { access_token } = await getAppToken({
clientId: "od_xxx",
clientSecret: "od_sec_xxx",
scopes: ["ip:read", "inventory:read"],
});React Auth Provider
New in v0.13.0 —
@goodz-core/sdk/react
For React apps, the Provider + Hook pattern gives you zero-config auth with automatic callback detection, session persistence, and auth state:
// main.tsx — wrap your app
import { GoodZAuthProvider } from "@goodz-core/sdk/react";
ReactDOM.createRoot(root).render(
<GoodZAuthProvider clientId="od_myapp">
<App />
</GoodZAuthProvider>
);// Any component
import { useGoodZAuth } from "@goodz-core/sdk/react";
function Profile() {
const { user, isAuthenticated, isLoading, login, logout } = useGoodZAuth();
if (isLoading) return <Spinner />;
if (!isAuthenticated) return <button onClick={() => login()}>Sign In</button>;
return (
<div>
<img src={user.picture} alt={user.name} />
<p>Hello, {user.name}!</p>
<button onClick={logout}>Sign Out</button>
</div>
);
}Provider props:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| clientId | string | Yes | Your OAuth client_id |
| coreBaseUrl | string | No | Core URL (default: https://goodzcore.manus.space) |
| redirectUri | string | No | Override redirect URI (default: window.location.origin) |
| scopes | string | No | OAuth scopes (default: "openid profile email") |
| exchangeCode | function | No | Custom code exchange (route through your backend) |
| onLogin | function | No | Called after successful login |
| onLogout | function | No | Called when logout is triggered |
| getUserInfo | function | No | Custom user info fetcher |
Hook return value:
| Field | Type | Description |
|-------|------|-------------|
| user | UserInfo \| null | Current user (null if not authenticated) |
| token | string \| null | Raw access token |
| tokenResponse | TokenResponse \| null | Full token response |
| isLoading | boolean | Auth state still being determined |
| isAuthenticated | boolean | Whether user is authenticated |
| error | Error \| null | Last auth error |
| login(options?) | function | Initiate login flow |
| logout() | function | Clear auth state |
Advanced: Route code exchange through your backend:
<GoodZAuthProvider
clientId="od_myapp"
exchangeCode={async (code, codeVerifier, redirectUri) => {
// Exchange via your backend (e.g., tRPC mutation)
// Backend can set HttpOnly cookie session
return await trpc.auth.exchangeGoodZCode.mutate({ code, codeVerifier, redirectUri });
}}
onLogin={async (tokens, user) => {
// Sync user to your database
await trpc.users.syncFromGoodZ.mutate({ openId: user.openId, name: user.name });
}}
>
<App />
</GoodZAuthProvider>Account Switching (v0.6.1+)
To force re-authentication (e.g., switch from Account A to Account B), pass forceLogin: true:
const switchUrl = buildAuthorizationUrl({
clientId: "od_myapp",
redirectUri: "https://myapp.com/callback",
forceLogin: true, // Appends prompt=login → clears Core session → shows login page
});This follows the OIDC prompt=login standard. The existing session is cleared server-side before the login page is shown, and the resulting authorization code is valid for the newly authenticated account.
Z-coin Precision
GoodZ stores Z-coin amounts in hundredths (1/100th of a Z-coin). For example, 10.50 Z-coin is stored as 1050.
import { toHundredths, toDisplay, formatZcoin } from "@goodz-core/sdk/zcoin";
toHundredths(10.50); // → 1050
toDisplay(1050); // → 10.5
formatZcoin(1050); // → "10.50"Always use toHundredths() when constructing API inputs:
// ✅ Correct
await goodz.zcoin.commercialTransfer({
priceZcoin: toHundredths(10.50), // 1050
// ...
});
// ❌ Wrong — this charges 0.10 Z-coin instead of 10.00
await goodz.zcoin.commercialTransfer({
priceZcoin: 10,
// ...
});Error Handling
All API errors are thrown as GoodZApiError instances:
import { GoodZApiError } from "@goodz-core/sdk";
try {
await goodz.zcoin.commercialTransfer({ /* ... */ });
} catch (err) {
if (err instanceof GoodZApiError) {
console.error(err.code); // "BAD_REQUEST", "FORBIDDEN", etc.
console.error(err.httpStatus); // 400, 403, etc.
console.error(err.path); // "zcoin.commercialTransfer"
console.error(err.message); // Human-readable message
if (err.zodErrors) {
for (const fieldErr of err.zodErrors) {
console.error(`${fieldErr.path.join(".")}: ${fieldErr.message}`);
}
}
}
}| Code | HTTP | Meaning | Action |
|------|------|---------|--------|
| BAD_REQUEST | 400 | Invalid input | Check input values |
| INSUFFICIENT_BALANCE | 400 | Not enough Z-coin | Redirect to e.depositUrl |
| UNAUTHORIZED | 401 | Missing or invalid token | Refresh token or re-login |
| FORBIDDEN | 403 | No permission | Show permission denied |
| NOT_FOUND | 404 | Resource doesn't exist | Show 404 or fallback |
| CONFLICT | 409 | Version conflict | Retry the operation |
Handling Insufficient Balance (Auto-Redirect to Deposit)
When a payment fails due to insufficient Z-coin, the error includes a deposit URL for seamless top-up:
import { GoodZApiError } from "@goodz-core/sdk";
try {
await goodz.zcoin.mintAndCharge({ /* gacha pull */ });
} catch (err) {
if (err instanceof GoodZApiError && err.isInsufficientBalance()) {
// Option 1: Redirect to Core's deposit page (simplest)
window.open(err.depositUrl, '_blank');
// Option 2: Build a custom deposit URL with return_url
const depositUrl = await goodz.zcoin.getDepositUrl({
amount: 100, // pre-select 100 Z-coin package
returnUrl: window.location.href,
appId: 'od_myapp',
});
window.open(depositUrl.url, '_blank');
}
}The isInsufficientBalance() helper and depositUrl getter work for all payment methods:
commercialTransfer, mintAndCharge, and chargeUser.
UI Components (React)
The SDK includes a React component for displaying GoodZ collectibles with full material effects and tilt interaction — the same experience used on GoodZ.Core itself.
GoodZCardFocus
import { GoodZCardFocus } from "@goodz-core/sdk/ui";
<GoodZCardFocus
imageUrl="https://goodzcore.manus.space/api/shell/42.png"
cardName="Luna"
rarity="SSR"
formFactor="trading_card"
>
<img src={thumbnailUrl} alt="Luna" className="w-full rounded-lg" />
</GoodZCardFocus>Supports all 10 form factors with unique material effects:
| Form Factor | Material Effect |
|---|---|
| trading_card | Holographic gradient + sparkle (intensity by rarity) |
| postcard / postcard_portrait | Paper texture + subtle shadow |
| polaroid | White border (thick bottom) + paper texture |
| laser_ticket | Intense rainbow holographic + shimmer animation |
| badge_* | Circular clip + metal frame + dome highlight |
| acrylic_stand_* | Transparent glass + edge refraction + caustic |
Interaction: desktop mouse tilt, mobile gyroscope (auto-detected), touch drag fallback, iOS permission prompt. React 18+ peer dependency. No CSS framework required.
Form Factor Utilities
import { FORM_FACTORS, getAspectRatioCSS, getFormFactorSpec } from "@goodz-core/sdk/ui";
getAspectRatioCSS("trading_card"); // "5 / 7"
getFormFactorSpec("polaroid"); // { key, label, aspectRatio: [3, 4], isCircle: false }Subpath Imports
For tree-shaking, import from specific subpaths:
import { createGoodZClient } from "@goodz-core/sdk/core";
import { TokenManager } from "@goodz-core/sdk/auth";
import { toHundredths } from "@goodz-core/sdk/zcoin";
import { GoodZCardFocus } from "@goodz-core/sdk/ui";
import { createCommerceNamespace } from "@goodz-core/sdk/commerce";
import { createExchangeNamespace } from "@goodz-core/sdk/exchange";
import { createAliveNamespace } from "@goodz-core/sdk/alive";
import { createShopsNamespace } from "@goodz-core/sdk/shops";The root import (@goodz-core/sdk) re-exports everything for convenience.
In-App Gacha Quick Start
This is the fastest path to adding GoodZ gacha (blind-box) or direct purchases to your app. The SDK handles all MCP wire format, auth token routing, SSE streaming, and error mapping — you never need to construct raw JSON-RPC requests.
Don't do this: If you're manually constructing MCP JSON-RPC requests to Commerce, stop. Use the SDK instead — it handles auth headers, SSE parsing,
isErrorresponses, and 401 auto-refresh automatically.
Prerequisites
npm install @goodz-core/sdkYou need:
- An OAuth
client_id(register your app on GoodZ.Core) - A Commerce campaign ID (created by the merchant via
assembleBatch+launchSalesCampaign+activateCampaign)
Complete Working Example (React)
// App.tsx — Wrap your app with GoodZAuthProvider
import { GoodZAuthProvider } from "@goodz-core/sdk/react";
export default function App() {
return (
<GoodZAuthProvider
clientId="od_your_app_id"
scopes="openid profile email" // ⚠️ "openid" is REQUIRED for Commerce
>
<GachaPage />
</GoodZAuthProvider>
);
}// GachaPage.tsx — Complete in-app gacha in ~30 lines
import { useGoodZAuth } from "@goodz-core/sdk/react";
import { createGoodZClient, GoodZApiError } from "@goodz-core/sdk";
import { useState } from "react";
const CAMPAIGN_ID = 42; // Your campaign ID
export function GachaPage() {
const { token, isAuthenticated, login } = useGoodZAuth();
const [result, setResult] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
if (!isAuthenticated) {
return <button onClick={() => login()}>Sign in to draw</button>;
}
const handleDraw = async () => {
setLoading(true);
setError(null);
try {
// SDK automatically sends user's JWT to Commerce as Bearer token
// No need to construct MCP JSON-RPC, handle SSE, or parse isError
const goodz = createGoodZClient({ accessToken: token! });
const draw = await goodz.commerce.executePurchase({
campaignId: CAMPAIGN_ID,
quantity: 1,
});
setResult(draw);
// draw.items[0].cardName, draw.items[0].imageUrl, draw.items[0].rarity
} catch (err) {
if (err instanceof GoodZApiError) {
if (err.isInsufficientBalance()) {
setError("Insufficient Z-coin balance. Please top up.");
// Optionally redirect to deposit:
// window.open(err.depositUrl!, "_blank");
} else {
setError(err.message); // Human-readable error from Commerce
}
} else {
setError("Unexpected error");
}
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={handleDraw} disabled={loading}>
{loading ? "Drawing..." : "Draw 1x"}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{result && (
<div>
<h3>You got: {result.items[0].cardName}</h3>
<img src={result.items[0].imageUrl} alt={result.items[0].cardName} />
<p>Rarity: {result.items[0].rarity}</p>
</div>
)}
</div>
);
}Server-Side Example (Node.js / Express)
import { createGoodZClient } from "@goodz-core/sdk";
import { TokenManager } from "@goodz-core/sdk/auth";
// For server-side: use TokenManager for automatic token refresh
const tokenManager = new TokenManager({
clientId: "od_your_app_id",
clientSecret: process.env.GOODZ_CLIENT_SECRET!,
initialTokens: {
accessToken: userAccessToken, // From OAuth callback
refreshToken: userRefreshToken, // From OAuth callback
expiresAt: tokenExpiresAt,
},
onTokenRefresh: async (tokens) => {
// Persist refreshed tokens to your database
await db.updateUserTokens(userId, tokens);
},
});
const goodz = createGoodZClient({ tokenManager });
// Execute purchase — SDK handles everything:
// ✅ Sends user JWT (not app M2M token) to Commerce
// ✅ Parses MCP JSON-RPC + SSE response format
// ✅ Checks result.isError and throws GoodZApiError
// ✅ Auto-refreshes token on 401 and retries
const draw = await goodz.commerce.executePurchase({
campaignId: 42,
quantity: 1,
});
console.log(draw.orderId); // number
console.log(draw.items[0].cardName); // string
console.log(draw.items[0].imageUrl); // string
console.log(draw.totalCharged); // number (Z-coin hundredths)What the SDK Handles For You
| Concern | Without SDK (manual) | With SDK |
|---------|---------------------|----------|
| MCP wire format | Construct JSON-RPC 2.0 envelope, set jsonrpc, method, params.name, params.arguments | Just call goodz.commerce.executePurchase({...}) |
| Auth header | Manually set Authorization: Bearer <token> | Automatic from accessToken or tokenManager |
| SSE streaming | Parse text/event-stream, extract last data: line | Handled internally |
| isError response | Check result.isError in MCP content | Throws GoodZApiError automatically |
| Token refresh on 401 | Detect 401, refresh token, rebuild headers, retry | Automatic with tokenManager |
| Insufficient balance | Parse error message for balance hint | err.isInsufficientBalance() + err.depositUrl |
| Commerce URL | Hardcode https://goodz-commerce.manus.space/api/mcp | Built-in default |
| Session management | Track mcp-session-id header | Automatic |
Common Mistakes
| Mistake | Symptom | Fix |
|---------|---------|-----|
| Using APP M2M token for purchase | "This campaign does not allow anonymous purchases" | Use user's access token, not client_credentials token |
| Missing openid in OAuth scope | Commerce can't identify the user | Add "openid" to scope: scopes="openid profile email" |
| Passing Core userId to Commerce | "User not found" or wrong user | Don't pass userId — SDK extracts it from the JWT automatically |
| Manually constructing MCP requests | Parsing errors, missed isError, no auto-refresh | Use the SDK |
Purchase Flow Guide
This section walks through the end-to-end purchase flow for a typical GoodZ-powered app.
Flow Overview
User visits merchant storefront → Browses campaigns → Selects item → Executes purchase
→ Z-coin deducted → GoodZ minted to user → Order confirmedStep 1: Display Merchant & Campaigns
// Get merchant info (no auth required)
const appInfo = await goodz.platform.getAppInfo({ clientId: "od_merchantapp" });
// Browse active campaigns
const campaigns = await goodz.commerce.searchMarketplace({
merchant_id: merchantId,
});Step 2: Execute Purchase
// Direct purchase (fixed-price batch)
const order = await goodz.commerce.executePurchase({
campaign_id: campaignId,
quantity: 1,
purchase_type: "direct",
});
// Gacha draw (blind-box)
const draw = await goodz.commerce.executePurchase({
campaign_id: gachaCampaignId,
quantity: 3,
purchase_type: "gacha",
});Step 3: Handle Insufficient Balance
import { GoodZApiError } from "@goodz-core/sdk";
try {
await goodz.commerce.executePurchase({ /* ... */ });
} catch (err) {
if (err instanceof GoodZApiError && err.isInsufficientBalance()) {
// Redirect to deposit page
const { url } = await goodz.zcoin.getDepositUrl({
returnUrl: window.location.href,
appId: "od_myapp",
});
window.open(url, "_blank");
}
}Step 4: Check Order Status
// Poll order status (useful for async/anonymous purchases)
const status = await goodz.commerce.checkOrderStatus({
orderId: order.orderId,
});
// status.status: "pending" | "confirmed" | "failed" | "refunded"Step 5: View Inventory
// First page of user's inventory
const page1 = await goodz.inventory.getMyInventory({ limit: 50 });
console.log(page1.items); // InventoryItem[]
// Next page via cursor (more efficient than offset for large collections)
if (page1.nextCursor) {
const page2 = await goodz.inventory.getMyInventory({
limit: 50,
cursor: page1.nextCursor,
});
}
// Filter by rarity + form factor
const ssrCards = await goodz.inventory.getMyInventory({
rarity: "SSR",
formFactor: "trading_card",
});
// Sort by card name A→Z
const alphabetical = await goodz.inventory.getMyInventory({
sortBy: "cardName",
sortOrder: "asc",
});
// Sort by franchise name, then paginate with offset
const byFranchise = await goodz.inventory.getMyInventory({
sortBy: "franchiseName",
sortOrder: "asc",
limit: 20,
offset: 0, // Use offset pagination for non-acquiredAt sorts
});
// Next page: offset: 20, then offset: 40, etc.
// Check byFranchise.hasMore to know if more pages exist
// Sort by instance number (ascending)
const byNumber = await goodz.inventory.getMyInventory({
sortBy: "instanceNumber",
sortOrder: "asc",
franchiseId: 42, // Combine with filters
});
// Inventory stats summary
const stats = await goodz.inventory.getStats();
console.log(`You own ${stats.totalInstances} cards across ${stats.totalFranchises} franchises`);
// Stats filtered by rarity
const ssrStats = await goodz.inventory.getStats({ rarity: "SSR" });Step 6: Secondary Market (Optional)
// List an item for sale on Exchange
await goodz.exchange.createListing({
instance_id: 12345,
listing_type: "fixed_price",
price_zcoin: toHundredths(50), // 50 Z-coin
});
// Browse marketplace
const listings = await goodz.exchange.browseMarketplace({
sort: "price_asc",
});Entry Points
The SDK is organized into focused entry points. Import only what you need:
| Entry Point | Import Path | Description |
|-------------|-------------|-------------|
| Main | @goodz-core/sdk | Types, createGoodZClient, SDK_VERSION |
| Core | @goodz-core/sdk/core | createGoodZClient (detailed) |
| Auth | @goodz-core/sdk/auth | Server-side: TokenManager, exchangeCode, revokeToken, buildAuthorizationUrl |
| Browser Auth | @goodz-core/sdk/browser-auth | Browser-side: GoodZBrowserAuth, getAppToken, clearAppTokenCache |
| React | @goodz-core/sdk/react | GoodZAuthProvider, useGoodZAuth (wraps browser-auth) |
| Z-coin | @goodz-core/sdk/zcoin | toHundredths, toDisplay, formatZcoin |
| UI | @goodz-core/sdk/ui | GoodZCardFocus React component |
| Commerce | @goodz-core/sdk/commerce | createCommerceClient |
| Exchange | @goodz-core/sdk/exchange | createExchangeClient |
| Alive | @goodz-core/sdk/alive | createAliveClient |
| Shops | @goodz-core/sdk/shops | createShopsNamespace |
Tree-shaking: Each entry point is a separate ESM chunk. Importing
@goodz-core/sdk/zcoindoes not pull in React or the full API client.
Migration from Standalone Files
If you were previously copying goodz-auth.ts and goodz-auth-react.tsx into your project, you can now use the npm package instead:
| Before (manual copy) | After (npm) |
|---------------------|-------------|
| import { GoodZBrowserAuth } from "./goodz-auth" | import { GoodZBrowserAuth } from "@goodz-core/sdk/browser-auth" |
| import { GoodZAuthProvider, useGoodZAuth } from "./goodz-auth-react" | import { GoodZAuthProvider, useGoodZAuth } from "@goodz-core/sdk/react" |
| import { getAppToken } from "./goodz-auth" | import { getAppToken } from "@goodz-core/sdk/browser-auth" |
The API is identical — just change the import path. The standalone files remain available for zero-dependency scenarios.
Migration from Raw MCP Calls
If you were previously calling Commerce/Exchange/Alive MCP endpoints directly via fetch() or curl, the SDK eliminates all that boilerplate:
Before (manual MCP JSON-RPC)
// ❌ Manual: 30+ lines of boilerplate per call
const res = await fetch("https://goodz-commerce.manus.space/api/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${userToken}`, // Must be USER token, not APP token!
"Accept": "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "tools/call",
params: { name: "execute_purchase", arguments: { campaignId: 42, quantity: 1 } },
id: 1,
}),
});
// Must handle SSE vs JSON response format
const contentType = res.headers.get("content-type") || "";
let body;
if (contentType.includes("text/event-stream")) {
const text = await res.text();
const lines = text.split("\n");
const lastData = lines.filter(l => l.startsWith("data:")).pop()?.slice(5).trim();
body = JSON.parse(lastData!);
} else {
body = await res.json();
}
// Must check isError in MCP content
if (body.result?.isError) {
throw new Error(body.result.content[0].text); // Easy to miss!
}
// Must extract JSON from MCP text content
const result = JSON.parse(body.result.content[0].text);After (SDK)
// ✅ SDK: 3 lines, fully typed, auto-refresh, error handling built-in
import { createGoodZClient, GoodZApiError } from "@goodz-core/sdk";
const goodz = createGoodZClient({ accessToken: userToken });
const result = await goodz.commerce.executePurchase({ campaignId: 42, quantity: 1 });
// result is typed as CommerceDrawResult — { orderId, items, totalCharged, platformFee }What the SDK handles that you no longer need to:
- MCP JSON-RPC envelope —
jsonrpc,method,params.name,params.arguments,id - SSE response parsing —
text/event-streamcontent type detection and extraction isErrorchecking — Automatically throwsGoodZApiErrorwhen Commerce returns an error- Session management —
mcp-session-idheader tracking across calls - 401 auto-refresh — When using
TokenManager, expired tokens are refreshed and retried - Commerce URL — Default
https://goodz-commerce.manus.spacebuilt-in - Type safety — Full TypeScript types for all inputs and outputs
Changelog
0.13.0 (2026-03-28)
Browser Auth + React Provider in npm
@goodz-core/sdk/browser-auth: New entry point —GoodZBrowserAuth(PKCE S256 + sessionStorage),getAppToken()(M2M with caching),clearAppTokenCache(). Zero Node.js dependencies, uses Web Crypto API.@goodz-core/sdk/react: New entry point —GoodZAuthProvider+useGoodZAuth()hook. Automatic callback detection, session persistence, auth state management. Supports customexchangeCodefor backend-routed token exchange.- Migration path: Developers who previously copied
goodz-auth.ts/goodz-auth-react.tsxcan nownpm installand change import paths. API is identical. - Entry Points table: Added comprehensive entry points documentation.
- React remains an optional peer dependency —
browser-authworks without React.
0.12.0 (2026-03-28)
Inventory Sorting
sortByparameter: Inventory queries (getMyInventory,getUserInventory) now acceptsortBywith values:"acquiredAt"(default),"cardName","seriesName","franchiseName","instanceNumber". Combine withsortOrder: "asc" | "desc"for full control.hasMorefield: Response now includeshasMore: booleanfor reliable pagination detection, especially useful with offset-based pagination when using non-acquiredAt sorts.franchiseNamein items: Each inventory item now includesfranchiseNamefor display without additional lookups.- Cursor + sort compatibility: Cursor-based pagination remains available for
acquiredAtsort (most efficient). For other sort fields, use offset-based pagination (offsetparameter). - MCP sync:
get_user_inventoryMCP tool also supportssort_byandsort_orderparameters. - New types:
InventorySortBy,SortOrderexported from SDK.
0.17.1 (2026-04-02)
Commerce V4.0 Merchant Migration — Patch
- JSDoc cleanup: Replaced all remaining "shop" / "shop owner" references in Commerce SDK JSDoc comments with "merchant" / "merchant owner" across
types-commerce.ts,commerce/index.ts, andREADME.md. - Dashboard type fix:
CommerceMerchantDashboard.stats.shopShare→merchantShareto match Commerce V4.0 actual response fieldtotalMerchantShareCents. - README examples: Updated all code examples from
createShop/shopIdtogetOrCreateMerchant/merchantId. Updated method reference table. - dist rebuild: Regenerated
dist/index.d.tsto eliminate stalecreateShopJSDoc in published types.
0.17.0 (2026-04-02)
Commerce V4.0 Merchant Migration
- BREAKING:
createShop→getOrCreateMerchant(idempotent). AllshopIdparams →merchantId.getShopDashboard→getMerchantDashboard.getShopsByBlueprint→getMerchantsByBlueprint.learnShop→learnMerchant.getShopownerSkill→getMerchantownerSkill. - New campaign params:
anonymousAllowed,maxPerDevice,maxPerUser,periodType,periodQuota,periodCustomDaysadded toCommerceLaunchCampaignInput. - Type renames: All
CommerceShop*types →CommerceMerchant*.CommerceCreateShopInput→CommerceGetOrCreateMerchantInput(name now optional).
0.16.1 (2026-04-01)
MCP Alignment — Full Sub-site SDK Sync
- Commerce (46 tools): Added 12 missing methods:
requestRedemption,approveRedemption,rejectRedemption,shipRedemption,confirmRedemptionReceived,cancelRedemption,getRedemptionStatus,getMyRedemptions,learnMerchant,getMerchantownerSkill,topUpWallet,listWebhooks. Removed orphanredeemPhysical(replaced byrequestRedemption). Fixed 6 parameter mismatches (updateCampaign+period,executePurchase+fingerprint/urls, etc.). - Shops (14 tools): Added 2 new methods:
getShopBranding,batchShopBranding. - Exchange (23 tools): Added 2 missing methods:
getWatchlist(now paginated),learnExchange. FixedbuyListing(+referenceId),getMarketData(+period). - Alive (9 tools): Added
learnAlivemethod. - Type exports: All new input/output types exported from respective namespace modules.
0.11.0 (2026-03-28)
Sprint 5 — Creator/Developer APIs + Settlement Namespace
inventory.batchMint(): Batch mint multiple cards in one call. Each item specifiescardIdandquantity(max 100 total instances). Requires franchise ownership. Returns per-card results.inventory.mintFor(): Authorized party mints instances. Two modes: claimed (toUserId provided → instant ownership) or unclaimed (toUserId omitted → returns claim tokens/URLs for anonymous distribution). Requires active mint authorization.inventory.mintAsGift(): Mint unclaimed instances with claim tokens for gift distribution. Requires franchise ownership. Returns claim URLs with optional expiry.inventory.revokeMintAuth(): Revoke a mint authorization. Cannot revoke if locked by an APP.inventory.listMintAuths(): List mint authorizations filtered bycardId,granteeUserId, orstatus.settlementnamespace: New namespace for creator revenue management:getMyBalance()— settlement credit balance with lifetime totalsgetMyHistory()— paginated transaction historyclaimAsZcoin()— convert settlement credits to spendable Z-coin
- Type exports:
SettlementNamespace,BurnNamespace,MintAuthorization, and all new input/output types.
0.10.0 (2026-03-28)
Sprint 4 — Rarity/FormFactor Filtering + Cursor Pagination
rarityfilter: All inventory endpoints (getMyInventory,getUserInventory,getStats) now accept ararityparameter for exact-match filtering (e.g."SSR","Rare"). Creator-defined labels.formFactorfilter: All inventory endpoints accept aformFactorparameter ("trading_card","badge_small", etc.) for filtering by physical form factor.- Cursor-based pagination:
getMyInventory()andgetUserInventory()now return{ items, nextCursor }instead of a flat array. PassnextCursorascursorin the next request for efficient keyset pagination. Offset-based pagination still supported for backward compatibility. FormFactorKeytype: Exported from SDK for type-safe form factor filtering.InventoryPagetype: New response type withitems: InventoryItem[]andnextCursor?: number.- Breaking change:
getMyInventory()andgetUserInventory()return shape changed fromInventoryItem[]toInventoryPage. Migrate:const items = (await goodz.inventory.getMyInventory()).items;
0.9.0 (2026-03-28)
Sprint 3 — PKCE S256 + Inventory Stats + Token Revocation + Purchase Guide
- PKCE S256:
buildAuthorizationUrl()is now async and computes S256 code challenges automatically. AddedgenerateCodeVerifier()andcomputeCodeChallenge()helpers. GoodZ.Core requires S256 — the oldplainmethod was never accepted by the server. inventory.getStats(): New endpoint returning inventory summary statistics (total instances, franchises, series) with breakdown by franchise → series. SupportsfranchiseIdfilter.revokeToken(): Standalone function andTokenManager.revokeToken()method for RFC 7009 token revocation on logout.- Purchase Flow Guide: End-to-end documentation covering merchant display → purchase → balance handling → order status → inventory → secondary market.
- Deprecation:
buildAuthorizationUrlSync()is deprecated (usesplainPKCE which Core rejects). Will be removed in v1.0.
0.8.0 (2026-03-28)
Sprint 2 — Inventory Filtering + Platform Info
franchiseIdfilter: AddedfranchiseIdparameter toinventory.getMyInventory()andinventory.getUserInventory(). Users can now filter their inventory by franchise (IP), the most requested feature for independent sites.platform.getAppInfo(): Newplatformnamespace withgetAppInfo({ clientId })— returns app/shop public info (name, description, icon, website URL). No auth required. Enables independent sites to display shop branding.- Backend: Added
franchiseIdfilter to tRPCinventory.mineandinventory.getUserInventory, and MCPget_user_inventory. Addedplatform.getAppInfotRPC endpoint andget_app_infoMCP tool.
0.7.0 (2026-03-28)
Sprint 1 — Convenience Methods
inventory.getMyInventory(): Convenience wrapper for authenticated user's own inventory (maps toinventory.mine). SupportsseriesId,franchiseId,limit,offset.commerce.checkOrderStatus(): Check order/checkout/batch status byorderId,checkoutSessionId, orbatchSessionId. Essential for anonymous purchase flows.ip.listFranchises(): List all published franchises with optionalownerId,limit,offsetfilters. No auth required.- Confirmed
commerce.listWebhooks()already exists.
0.6.1 (2026-03-28)
- Account Switching: Added
forceLogin?: booleantoOAuthUrlConfiginbuildAuthorizationUrl(). Whentrue, appendsprompt=loginto the authorize URL, forcing re-authentication (OIDC standard). Requires GoodZ.Core v2.18+.
0.6.0
- Initial stable release with Commerce, Exchange, Alive namespaces
License
MIT
