@b3dotfun/upside-sdk
v0.0.31
Published
Integrating with Upside.win is simple. Your game runs in an iframe on our platform, receives player authentication via JWT, and interacts with the Upside backend through our SDK for bet placement and payout processing.
Keywords
Readme
Overview
Integrating with Upside.win is simple. Your game runs in an iframe on our platform, receives player authentication via JWT, and interacts with the Upside backend through our SDK for bet placement and payout processing.
Key principle: Your backend handles game logic and state (cards, winners, outcomes), while Upside handles all WIN token transactions.
Integration Flow
```text
Contact: [email protected]
Include: Game name, description, game type, and your backend URL
``````javascript
import { UpsideProvider } from "@b3dotfun/upside-sdk";
export default function GameApp() {
return (
<UpsideProvider>
<YourGameComponent />
</UpsideProvider>
);
}
``````javascript
{ createB3Client } from "@b3dotfun/upside-sdk/server";
const b3Client = createB3Client();
const betResult = await
b3Client.placeBet(
"coin-flip", // gameType
"100000000000000000" // betAmount in wei (1 token = 10^18 wei)
);
``````javascript
const payoutResult = await b3Client.processPayout(
"coin-flip", // gameType
sessionId, // unique game session ID
payoutAmount, // WIN tokens to award (0 if loss)
// gameData
{
playerChoice: "heads",
result: "heads",
outcome: "win"
}
);
```The Upside platform automatically:
- Updates the player's WIN balance
- Adds the win/loss to leaderboards
- Sends notificationsSDK Setup
Installation
npm install @b3dotfun/upside-sdk
# or
pnpm add @b3dotfun/upside-sdk
# or
bun install @b3dotfun/upside-sdkFrontend: UpsideProvider
The UpsideProvider component connects your game frontend to the Upside platform and provides authentication.
import {
UpsideProvider,
useUpside,
useBalance,
useToken,
useAuthenticatedFetch,
} from "@b3dotfun/upside-sdk";
export default function CoinFlipGame() {
return (
<UpsideProvider>
<GameContent />
</UpsideProvider>
);
}
function GameContent() {
const { token, balance, showWinModal, showLossModal, refetchBalance } =
useUpside();
// Or use individual hooks:
const balance = useBalance(); // number | null
const token = useToken(); // string | null
const authFetch = useAuthenticatedFetch(); // fetch with Authorization header
return (
<div>
<h1>Coin Flip</h1>
<p>Balance: {(balance / 1e18).toFixed(2)} WIN</p>
<button onClick={() => playGame(token)}>Play</button>
</div>
);
}Available Hooks:
useUpside(): Access full context (token, balance, showWinModal, showLossModal, etc.)useBalance(): Get player balance (number | null)useToken(): Get authentication token (string | null)useRefetchBalance(): Function to request balance refreshuseCustomModal(): Function to show custom modals with React componentsuseAuthenticatedFetch(): Fetch function with automatic Bearer token header
Return Values from useUpside():
token(string | null): JWT authentication token for backend callsbalance(number | null): Current WIN token balance in weishowWinModal(wins: string): Show win modalshowLossModal(loss: string): Show loss modalshowToast(options): Show toast notificationshowCustomModal(content): Show custom modalrefetchBalance(): Request balance refresh from platform
Making API Calls from Frontend
Use the useAuthenticatedFetch() hook to make authenticated requests:
import {
useAuthenticatedFetch,
useBalance,
useToken,
} from "@b3dotfun/upside-sdk";
function GameComponent() {
const authFetch = useAuthenticatedFetch();
const balance = useBalance();
const token = useToken();
const playGame = async (prediction) => {
// Authorization header is automatically added
const response = await authFetch("/api/game/coin-flip", {
method: "POST",
body: JSON.stringify({
playerId: "player-id",
prediction,
betAmount: "1000000000000000000", // 1 WIN in wei
}),
});
const data = await response.json();
return data;
};
return (
<div>
<p>Balance: {(balance / 1e18).toFixed(2)} WIN</p>
<button onClick={() => playGame("heads")}>Predict Heads</button>
</div>
);
}How useAuthenticatedFetch() Works:
- Automatically includes
Authorization: Bearer {token}header - Sets
Content-Type: application/jsonby default - Merges additional headers you provide
- Handles token from
useToken()automatically
Showing Custom Modals
Use the useCustomModal() hook to display custom modal content in the Upside platform.
import { useCustomModal } from "@b3dotfun/upside-sdk";
function GameComponent() {
const showCustomModal = useCustomModal();
const showHelp = () => {
showCustomModal(
<div>
<h2>How to Play</h2>
<p>Choose heads or tails and place your bet!</p>
<ul>
<li>Win: 1.5x your bet</li>
<li>Loss: Lose your bet</li>
</ul>
</div>,
);
};
const showCustomStyledModal = () => {
showCustomModal({
content: (
<div>
<h2>Special Bonus!</h2>
<p>You've unlocked a 2x multiplier!</p>
</div>
),
className: "bonus-modal",
});
};
return (
<div>
<button onClick={showHelp}>Show Help</button>
<button onClick={showCustomStyledModal}>Show Bonus</button>
</div>
);
}Usage:
The hook accepts either:
Simple ReactNode: Pass any React component or JSX directly
showCustomModal(<div>Simple content</div>);Options Object: Pass an object with
contentand optionalclassNameshowCustomModal({ content: <div>Custom content</div>, className: "my-custom-class", });
How it works:
- React components are rendered to static HTML using
renderToStaticMarkup - The HTML is sent to the parent Upside platform via postMessage
- The platform displays it in a modal overlay
- Optional
classNameallows custom styling
Best Practices:
- Keep modal content simple and focused
- Use for game rules, achievements, special notifications
- Avoid complex interactive components (use for display only)
- CSS classes should match your game's styling theme
Backend: createB3Client
Initialize the B3 client in your backend to interact with Upside's API.
Framework Support: Currently supports Hono with Cloudflare Workers.
import { createB3Client } from "@b3dotfun/upside-sdk/server";
// In your Hono route handler
app.post("/api/game/play", async (c) => {
// Create B3Client from Hono context
// Automatically extracts auth token from Authorization header
const b3Client = createB3Client(c);
// Now use b3Client for game operations
const betResult = await b3Client.placeBet("coin-flip", "1000000000000000000");
return c.json({ success: true, sessionId: betResult.sessionId });
});Parameters:
context(Hono Context): The Hono context object containing:req.header(name: string): Method to extract request headersenv(optional): Cloudflare Workers environment
Returns:
- B3Client instance with authentication automatically configured from the Authorization header
Authentication: The Authorization header is automatically extracted from the incoming request:
Authorization: Bearer {player_token}Testing on Localhost
Quick Testing Setup
You can test your game running on localhost directly in the Upside.win test environment without deploying.
How it works:
- Run your game backend on
http://localhost:3000(or any port) - Base64 encode your localhost URL
- Navigate to the test URL on upside.win
Step-by-Step
Step 1: Start your game backend
npm run dev
# Game running at http://localhost:3000Step 2: Base64 encode your URL
Using Node.js:
const url = "http://localhost:3000";
const encoded = Buffer.from(url).toString("base64");
console.log(encoded); // aHR0cDovL2xvY2FsaG9zdDozMDAwUsing command line:
echo -n "http://localhost:3000" | base64
# aHR0cDovL2xvY2FsaG9zdDozMDAwOnline: Use any base64 encoder at https://www.base64encode.org/
Step 3: Test in Upside.win
Visit the test URL:
https://upside.win/test/games/aHR0cDovL2xvY2FsaG9zdDozMDAwReplace aHR0cDovL2xvY2FsaG9zdDozMDAw with your encoded URL.
Examples
Different localhost URLs:
| URL | Base64 | Test Link |
| ----------------------- | ------------------------------ | ------------------------------------------------------------ |
| http://localhost:3000 | aHR0cDovL2xvY2FsaG9zdDozMDAw | https://upside.win/test/games/aHR0cDovL2xvY2FsaG9zdDozMDAw |
| http://localhost:5000 | aHR0cDovL2xvY2FsaG9zdDo1MDOw | https://upside.win/test/games/aHR0cDovL2xvY2FsaG9zdDo1MDOw |
| http://127.0.0.1:3000 | aHR0cDovLzEyNy4wLjAuMTozMDAw | https://upside.win/test/games/aHR0cDovLzEyNy4wLjAuMTozMDAw |
Development Workflow
- Create game code - Write your React frontend and Cloudflare Hono backend
- Start locally - Run
npm run devon localhost - Generate test URL - Base64 encode your localhost address
- Test on Upside - Visit
https://upside.win/test/games/<BASE64> - Get JWT & test - Game loads with real JWT for testing
- Iterate - Make changes locally and refresh the test URL
- Deploy - When ready, deploy backend and update game URL
Tips for Local Testing
- Use same machine: Keep localhost running while testing
- Check CORS: Ensure your backend allows requests from upside.win domains
Troubleshooting Local Testing
Problem: "Game not found" or 404
- Solution: Verify localhost URL is correct and encoding is accurate
Problem: CORS errors
- Solution: Your backend needs to accept requests from upside.win domain
Problem: JWT errors during testing
- Solution: Make sure you're using staging API key, not production
Problem: Network requests failing
- Solution: Check that localhost is running and firewall allows requests
Core Functions
placeBet
Start a game session by placing a bet. This locks in the wager and creates a game session.
const betResult = await b3Client.placeBet(
gameType, // string: "coin-flip", "dice", etc.
betAmount, // string: bet in wei (e.g., "100000000000000000" = 1 token)
);
// Response:
// {
// sessionId: "session_abc123",
// gameId: "game_xyz789",
// status: "active",
// createdAt: "2024-01-15T10:30:00Z"
// }Parameters:
gameType(string): Your game's identifier (e.g., "coin-flip", "dice-roll")betAmount(string): Bet amount in wei (1 token = 10^18 wei, e.g., "100000000000000000")
Returns:
sessionId: Unique identifier for this game sessiongameId: Game record identifierstatus: Current session status ("active", "completed", "failed")createdAt: ISO timestamp of bet creation
Errors:
- Insufficient player balance
- Invalid game type
- Game not active/enabled
- Invalid bet amount
processPayout
Complete the game and credit the player's winnings.
const payoutResult = await b3Client.processPayout(
gameType, // string: "coin-flip", etc.
sessionId, // string: from placeBet response
payoutAmount, // string: WIN to award in wei (0 for loss)
{
playerChoice: "heads", // what player chose/predicted
result: "heads", // actual game result
outcome: "win", // "win" or "loss"
},
);
// Response:
// {
// status: "completed",
// payoutAmount: "150000000000000000",
// newBalance: "1350000000000000000",
// updatedAt: "2024-01-15T10:30:30Z"
// }Parameters:
gameType(string): Same game type from placeBetsessionId(string): Session ID from placeBet responsepayoutAmount(string): WIN tokens to credit in wei (0 for losses, e.g., "150000000000000000" = 1.5 tokens)metadata(object):playerChoice: What the player chose/predictedresult: The actual game outcomeoutcome: "win" or "loss"
Returns:
status: "completed", "failed", etc.payoutAmount: Amount credited in weinewBalance: Player's updated WIN balance in weiupdatedAt: ISO timestamp of completion
Errors:
- Session not found
- Session already completed (duplicate request)
- Payout exceeds pool limits
- Invalid bet amount format
Complete Example: Coin Flip Game
Frontend (React)
import { UpsideProvider, useUpsideContext } from "@b3dotfun/upside-sdk";
export default function CoinFlipGame() {
return (
<UpsideProvider>
<CoinFlipContent />
</UpsideProvider>
);
}
function CoinFlipContent() {
const { token, balance, playerId } = useUpsideContext();
const [gameState, setGameState] = useState("ready"); // ready, playing, won, lost
const [prediction, setPrediction] = useState(null);
const [result, setResult] = useState(null);
const [earnings, setEarnings] = useState(0);
const playGame = async (playerPrediction) => {
setPrediction(playerPrediction);
setGameState("playing");
try {
// Call your backend
const response = await fetch("/api/game/coin-flip", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
playerId,
prediction: playerPrediction,
betAmount: "100000000000000000", // 1 token in wei
}),
});
const data = await response.json();
if (data.outcome === "win") {
setGameState("won");
setEarnings(data.payout);
} else {
setGameState("lost");
setEarnings(0);
}
setResult(data.result);
} catch (error) {
console.error("Game error:", error);
setGameState("error");
}
};
return (
<div style={{ textAlign: "center", padding: "40px" }}>
<h1>Coin Flip</h1>
<p>Balance: {(balance / 1e18).toFixed(2)} WIN</p>
{gameState === "ready" && (
<div>
<button onClick={() => playGame("heads")}>Predict Heads</button>
<button onClick={() => playGame("tails")}>Predict Tails</button>
</div>
)}
{gameState === "playing" && <p>Flipping...</p>}
{gameState === "won" && (
<div>
<p>?? You won! Coin landed on {result}</p>
<p>+{(earnings / 1e18).toFixed(2)} WIN</p>
</div>
)}
{gameState === "lost" && (
<div>
<p>? You lost! Coin landed on {result}</p>
<p>Better luck next time</p>
</div>
)}
{gameState !== "ready" && (
<button onClick={() => setGameState("ready")}>Play Again</button>
)}
</div>
);
}Backend (Hono + Cloudflare Workers)
import { Hono } from "hono";
import { createB3Client } from "@b3dotfun/upside-sdk/server";
const app = new Hono();
app.post("/api/game/coin-flip", async (c) => {
const { prediction, betAmount } = await c.req.json();
try {
// Create B3Client from Hono context
// Automatically extracts auth token from Authorization header
const b3Client = createB3Client(c);
// Step 1: Place the bet (amount in wei)
const betResult = await b3Client.placeBet("coin-flip", betAmount);
if (!betResult.sessionId) {
return c.json({ error: "Failed to place bet" }, 400);
}
// Step 2: Game logic - flip coin
const coin = Math.random() < 0.5 ? "heads" : "tails";
const isWin = coin === prediction;
// Calculate payout: 50% profit on win (betAmount * 1.5, in wei)
const payout = isWin
? (BigInt(betAmount) * BigInt(150)) / BigInt(100)
: "0";
// Step 3: Store game in your database (D1, etc.)
// await db.execute(
// "INSERT INTO games (sessionId, prediction, result, betAmount, payout) VALUES (?, ?, ?, ?, ?)",
// [betResult.sessionId, prediction, coin, betAmount, payout.toString()]
// );
// Step 4: Process payout
const payoutResult = await b3Client.processPayout(
"coin-flip",
betResult.sessionId,
payout.toString(),
{
playerChoice: prediction,
result: coin,
outcome: isWin ? "win" : "loss",
},
);
// Step 5: Return result to frontend
return c.json({
sessionId: betResult.sessionId,
prediction,
result: coin,
outcome: isWin ? "win" : "loss",
payout: isWin ? payout.toString() : "0",
newBalance: payoutResult.newBalance,
});
} catch (error) {
console.error("Game error:", error);
return c.json({ error: error.message }, 500);
}
});
export default app;Key Differences from Express:
createB3Client(c)extracts auth automatically from Hono context- Runs on Cloudflare Workers (serverless)
- No need for manual middleware - Hono context handles everything
- Response uses
c.json()instead ofres.json()
Environment Setup (wrangler.toml):
[env.production]
name = "upside-games"
route = "example.com/api/*"
zone_id = "..."
account_id = "..."
[[env.production.env.vars]]
# Add any environment variables hereBest Practices
Bet Placement
- Always validate amounts: Check bet is within player balance
- Use idempotency: Retry failed
placeBetcalls with the same sessionId - Lock immediately: Once
placeBetsucceeds, prevent player from placing another bet
Game Logic
- Backend is source of truth: Never trust client-side game outcomes
- Store everything: Log all game events for audits and disputes
- Validate results: Ensure game outcome matches expected range
- Timeout games: Cancel bets if no payout is processed within 5 minutes
Payout Processing
- Process once: Only call
processPayoutonce per game session - Use correct amounts: Verify payout calculation before sending
- Handle duplicates: If
processPayoutreturns "already completed", that's OK - Handle failures: Retry failed payouts, but check if already paid first
Security
- Verify tokens: Always validate JWT in every backend request
- Use HTTPS: All communication must be encrypted
- Validate game types: Only allow known, approved game types
- Rate limit: Implement rate limiting to prevent abuse
Error Handling
async function safePlayGame(gameType, betAmount) {
try {
// Place bet
let betResult;
try {
betResult = await b3Client.placeBet(gameType, betAmount);
} catch (error) {
if (error.message.includes("Insufficient balance")) {
return { error: "Player balance too low" };
}
throw error;
}
// Run game logic
const outcome = await runGameLogic();
// Process payout
try {
const payout = outcome.isWin ? betAmount * 1.5 : "0";
await b3Client.processPayout(gameType, betResult.sessionId, payout, {
playerChoice: outcome.choice,
result: outcome.result,
outcome: outcome.isWin ? "win" : "loss",
});
} catch (error) {
if (error.message.includes("already completed")) {
console.log("Payout already processed for session");
} else {
throw error;
}
}
return outcome;
} catch (error) {
console.error("Game error:", error);
throw error;
}
}Troubleshooting
Common Issues
Problem: placeBet fails with "Insufficient balance"
- Solution: Check player balance before placing bet, or increase bet amount display in UI
Problem: processPayout returns "session not found"
- Solution: Verify sessionId matches bet response, check for typos
Problem: Duplicate game sessions or bets
- Solution: Use same sessionId for retries, implement idempotency on your end
Problem: JWT expires during gameplay
- Solution: Refresh token before game starts, handle token expiration gracefully
Problem: Game logic runs on client, leading to cheating
- Solution: Move ALL game logic to backend, client only displays results
