Skip to main content

Installation Guide

This guide walks you through the process of integrating Crystara's Blind Box SDK into your web application. For the most up-to-date information, refer to the official NPM package.

Step 1: Install the SDK Package

Start by installing the Crystara SDK package:

npm install crystara-sdk

Step 2: Configure Tailwind CSS

The SDK requires Tailwind CSS for styling components:

npm install tailwindcss

Required Tailwind Configuration

Configure your tailwind.config.ts with the following brand variables:

theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
brand: {
primary: "#6E44FF",
secondary: "#FF44E3",
accent: "#44FFED",
dark: "#1A1A2E",
light: "#F7F7FF",
},
},
},
},

You can customize these colors to match your brand's identity.

Step 3: Copy API Routes

Copy these required API routes from our example repository:

  • batch-metadata/route.ts
  • lootbox/route.ts
  • lootbox-info/route.ts
  • metadata/route.ts
  • whitelist-amount/route.ts

These routes protect your API key while providing the required functionality to the client.

Step 4: Environment Configuration

Add these environment variables to your project:

CRYSTARA_PRIVATE_API_KEY=<< SEND A REGISTRATION EMAIL TO [email protected] with 1. Purpose and 2. Registration Email >>

NEXT_PUBLIC_CRYSTARA_API_URL=https://api.crystara.trade/mainnet
NEXT_PUBLIC_SUPRA_RPC_URL="https://rpc-mainnet.supra.com/rpc/v1"

NEXT_PUBLIC_CRYSTARA_ADR=0xfd566b048d7ea241ebd4d28a3d60a9eaaaa29a718dfff52f2ff4ca8581363b85
NEXT_PUBLIC_COLLECTIONS_MODULE_NAME=crystara_blindbox_v1

NEXT_PUBLIC_SUPRA_CHAIN_ID=8

To obtain your CRYSTARA_PRIVATE_API_KEY, contact [email protected].

Step 5: Create a Blind Box on Crystara

Before integrating with your website:

  1. Create your blind box collection via Crystara's Creator Portal
  2. Note the URL of your collection (e.g., https://crystara.trade/marketplace/mycollection)
    • You'll need the collection name (e.g., mycollection) for your integration

Step 6: Wallet Integration

The SDK uses an event-based architecture for transactions. When a user clicks the buy button, a TRANSACTION_START event is emitted, which your code must handle to send the transaction to your wallet provider.

Step 7: Create the BlindBoxClient Component

Create a BlindBoxClient.tsx component that:

  • Handles wallet connections
  • Processes transaction events
  • Provides API wrapper functions
  • Renders the BlindBoxPage component
"use client";

import { BlindBoxPage, WALLET_EVENTS } from 'crystara-sdk';
import { walletEvents } from 'crystara-sdk';
import useStarkeyWallet from '@/app/hooks/starkeyExampleHook';
import { useEffect } from 'react';
import { toast, Toaster } from 'sonner';
import { motion } from 'framer-motion';

export function BlindBoxClient() {
const starkey = useStarkeyWallet();

useEffect(() => {
let lastTxHash = '';

const transactionStartEvent = async (event: any) => {
if (!event || !event.contractAddress || !event.moduleName || !event.functionName) {
console.error("Invalid transaction event data:", event);
return;
}

const moduleAddress = event.contractAddress;
const moduleName = event.moduleName;
const functionName = event.functionName;
const params = event.args || [];
const runTimeParams = event.typeArgs || [];

try {
const tx = await starkey.sendRawTransaction(
moduleAddress,
moduleName,
functionName,
params,
runTimeParams
);

if (tx && tx !== lastTxHash) {
lastTxHash = tx;
console.log("Transaction successful:", tx);

const txHash = tx;
walletEvents.emit(WALLET_EVENTS.TRANSACTION_SUCCESS, {txHash});

setTimeout(() => {
if (lastTxHash === txHash) {
lastTxHash = '';
}
}, 5000);
}
} catch (error) {
console.error("Transaction failed:", error);
}
};

const toastHandler = (event: any) => {
console.log("Notification received:", event);
if(event.type === "win") {
showCustomWinningToast(event);
}
};

const unsubscribe = walletEvents.on(WALLET_EVENTS.TRANSACTION_START, transactionStartEvent);
const unsubscribe2 = walletEvents.on(WALLET_EVENTS.NOTIFICATION, toastHandler);

return () => {
unsubscribe();
unsubscribe2();
};
}, [starkey]);

const showCustomWinningToast = (event: any) => {
toast.custom((t) => (
<motion.div
initial={{ opacity: 0, y: -100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -100 }}
className="bg-brand-dark p-6 rounded-lg shadow-xl"
>
<h3 className="text-xl font-bold text-white mb-2">Congratulations!</h3>
<img
src={event.image || ''}
alt={event.name}
className="w-32 h-32 object-cover rounded-lg mb-2"
/>
<p className="text-white">{event.name}</p>
<p className="text-gray-400">{event.rarity}</p>
</motion.div>
), {
duration: 3500,
});
}

// API wrapper functions
const fetchMetadata = async (url: string) => {
const response = await fetch(`/api/metadata?url=${encodeURIComponent(url)}`);
if (!response.ok) throw new Error('Failed to fetch metadata');
return response.json();
};

const fetchLootboxStats = async (url: string, viewer?: string) => {
const response = await fetch(`/api/lootbox?url=${encodeURIComponent(url)}${viewer ? `&viewer=${viewer}` : ''}`);
if (!response.ok) throw new Error('Failed to fetch lootbox stats');
return response.json();
};

const batchFetchMetadata = async (urls: string[], bustCache?: boolean) => {
const response = await fetch('/api/batch-metadata', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ urls, bustCache }),
});
if (!response.ok) throw new Error('Failed to batch fetch metadata');
return response.json();
};

const fetchLootboxInfo = async (lootboxCreatorAddress: string, collectionName: string, viewerAddress: string | null) => {
const response = await fetch('/api/lootbox-info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ lootboxCreatorAddress, collectionName, viewerAddress }),
});
if (!response.ok) throw new Error('Failed to fetch lootbox info');
return response.json();
};

const fetchWhitelistAmount = async (lootboxCreatorAddress: string, collectionName: string, currentAddress: string) => {
const response = await fetch('/api/whitelist-amount', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ lootboxCreatorAddress, collectionName, currentAddress }),
});
if (!response.ok) throw new Error('Failed to fetch whitelist amount');
return response.json();
};

return (
<div className="min-h-screen bg-black">
<BlindBoxPage
params={{
contractAddress: process.env.NEXT_PUBLIC_CRYSTARA_ADR || "",
contractModuleName: process.env.NEXT_PUBLIC_COLLECTIONS_MODULE_NAME || "",
supraRPCUrl: process.env.NEXT_PUBLIC_SUPRA_RPC_URL || "",
lootboxUrl: "uglies",
viewerAddress: starkey.accounts[0],
sounds: {
"open": "/sounds/opencrate.mp3",
"win": "/sounds/woosh.wav",
"tick": "/sounds/cratetick.mp3",
"error": "/sounds/error.mp3"
}
}}
fetchMetadata={fetchMetadata}
fetchLootboxStats={fetchLootboxStats}
batchFetchMetadata={batchFetchMetadata}
fetchLootboxInfo={fetchLootboxInfo}
fetchWhitelistAmount={fetchWhitelistAmount}
/>
<Toaster />
</div>
);
}

Step 8: Sound Effects

Add sound effects to enhance the blind box experience:

  1. Create a public/sounds directory
  2. Add sound files for different interactions:
    • opencrate.mp3 - When opening a box
    • woosh.wav - When winning an item
    • cratetick.mp3 - For animation ticks
    • error.mp3 - For error states

Complete Example Repository

For a full working example of the Crystara SDK integration, check out our NextJS Example Repository. This repository includes:

  • Basic Starkey (Supra) wallet connection and transaction handling
  • Ready-to-use API routes with imported server actions
  • Environment file setup for proper configuration
  • Tailwind config with Crystara's brand colors (which you can customize to match your brand)
  • Sound effects used for the blind box animation experience

By cloning this repository, you'll get a fully functional blind box spinner for the default "Uglies" collection, providing the same experience as Crystara's official marketplace.

Additional Resources

For technical support or questions, contact our developer support team at [email protected].