Documentation Index
Fetch the complete documentation index at: https://podium.build/docs/llms.txt
Use this file to discover all available pages before exploring further.
Build the canonical loyalty surface — a web or native app where users see their points, browse earned rewards, redeem collectibles, and track their engagement history. Everything reads from Podium’s points ledger and reward APIs.
What You’ll Build
Prerequisites
npm install @podium-sdk/node-sdk
import { createPodiumClient } from '@podium-sdk/node-sdk';
const client = createPodiumClient({
apiKey: process.env.PODIUM_API_KEY,
});
Step 1: Points Dashboard
The points ledger is double-entry — every earn and spend is tracked with full audit trail.
async function getWalletDashboard(userId: string) {
const points = await client.user.listPoints({ id: userId });
return {
balance: points.balance,
totalEarned: points.totalEarned,
totalSpent: points.totalSpent,
transactions: points.transactions,
};
}
React Component
function PointsDashboard({ userId }: { userId: string }) {
const { data, isLoading } = useQuery({
queryKey: ['points', userId],
queryFn: () => getWalletDashboard(userId),
});
if (isLoading) return <Skeleton />;
return (
<div className="rounded-2xl bg-gradient-to-br from-indigo-600 to-purple-700 p-6 text-white">
<p className="text-sm opacity-80">Available Points</p>
<p className="text-4xl font-bold">{data.balance.toLocaleString()}</p>
<div className="mt-4 flex gap-6 text-sm">
<div>
<p className="opacity-60">Earned</p>
<p className="font-medium">{data.totalEarned.toLocaleString()}</p>
</div>
<div>
<p className="opacity-60">Redeemed</p>
<p className="font-medium">{data.totalSpent.toLocaleString()}</p>
</div>
</div>
</div>
);
}
Step 2: On-Chain Collectibles
Users earn on-chain rewards (collectibles, event passes, tier badges) through campaigns, purchases, and engagement. Display them in a gallery.
async function getUserCollectibles(userId: string) {
const nfts = await client.userNfts.list({ id: userId });
return nfts;
}
Collectible Card Component
function CollectibleCard({ nft }: { nft: any }) {
return (
<div className="overflow-hidden rounded-xl border">
<img src={nft.imageUrl} alt={nft.name} className="aspect-square w-full object-cover" />
<div className="p-3">
<h3 className="font-semibold">{nft.name}</h3>
<p className="text-sm text-gray-500">{nft.type}</p>
{nft.redeemable && (
<button className="mt-2 w-full rounded-lg bg-indigo-600 py-2 text-sm text-white">
Redeem
</button>
)}
</div>
</div>
);
}
function CollectiblesGrid({ userId }: { userId: string }) {
const { data } = useQuery({
queryKey: ['collectibles', userId],
queryFn: () => getUserCollectibles(userId),
});
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
{data?.nfts?.map((nft: any) => (
<CollectibleCard key={nft.id} nft={nft} />
))}
</div>
);
}
Step 3: Earned Rewards
Track all rewards a user has earned — including those pending delivery.
async function getUserRewards(userId: string) {
const rewards = await client.user.listEarnedRewards({ id: userId });
return rewards;
}
Reward Analytics (Merchant View)
If you’re building the merchant-facing side, track reward performance:
async function getRewardAnalytics(rewardId: string) {
const [growth, redemptions, delivered] = await Promise.all([
client.earnedReward.getGrowth({ id: rewardId, timeRange: '1M' }),
client.earnedReward.getRedemption({ id: rewardId, timeRange: '1M' }),
client.earnedReward.getDelivered({ id: rewardId }),
]);
return { growth, redemptions, delivered };
}
Step 4: Transaction History
Show a feed of all points activity — earns, spends, and pending transactions.
function TransactionFeed({ transactions }: { transactions: any[] }) {
return (
<div className="divide-y">
{transactions.map((tx: any) => (
<div key={tx.id} className="flex items-center justify-between py-3">
<div>
<p className="font-medium">{tx.description}</p>
<p className="text-sm text-gray-500">
{new Date(tx.createdAt).toLocaleDateString()}
</p>
</div>
<span className={tx.type === 'EARN' ? 'text-green-600' : 'text-red-500'}>
{tx.type === 'EARN' ? '+' : '-'}{tx.amount}
</span>
</div>
))}
</div>
);
}
Step 5: Redeem at Checkout
When a user wants to spend points, check the maximum discount and apply it.
async function redeemPoints(userId: string, orderId: string, points: number) {
const discount = await client.userOrder.getDiscount({
id: userId,
orderId,
});
const safePoints = Math.min(points, discount.maxPoints);
const checkout = await client.userOrder.checkout({
id: userId,
orderId,
requestBody: { points: safePoints },
});
return {
clientSecret: checkout.clientSecret,
amount: checkout.amount,
pointsApplied: safePoints,
saved: discount.maxDiscount,
};
}
For native iOS apps, expose a simple JSON endpoint that WidgetKit can poll:
// API route: GET /api/widget/points/:userId
import { Hono } from 'hono';
const app = new Hono();
app.get('/api/widget/points/:userId', async (c) => {
const userId = c.req.param('userId');
const points = await client.user.listPoints({ id: userId });
return c.json({
balance: points.balance,
lastEarned: points.transactions?.[0]?.amount ?? 0,
lastEarnedAt: points.transactions?.[0]?.createdAt ?? null,
});
});
The WidgetKit timeline provider fetches this endpoint and renders a compact balance display on the home screen.
Putting It Together
function LoyaltyWallet({ userId }: { userId: string }) {
return (
<div className="mx-auto max-w-md space-y-6 p-4">
<PointsDashboard userId={userId} />
<section>
<h2 className="mb-3 text-lg font-bold">My Collectibles</h2>
<CollectiblesGrid userId={userId} />
</section>
<section>
<h2 className="mb-3 text-lg font-bold">Recent Activity</h2>
<TransactionFeed transactions={[]} />
</section>
</div>
);
}