Your First OApp — Build a Complete App
We'll build OASIS Quest Log — a web app where players log completed quests, earn karma and receive a commemorative NFT for each milestone. It uses every concept from Modules 1–5.
What you'll build
- A single-page web app with OASIS Avatar SSO login
- Quest submission form that saves to a Holon
- Karma award on quest completion
- Automatic NFT mint for milestone quests (every 10th completion)
- Personal dashboard showing karma, level and quest history
Prerequisites
- Completed Modules 1–5 (or confident with auth, karma, NFTs, data)
- Node.js 18+ with a simple HTTP server (or serve the HTML statically)
- An OASIS account with Wizard avatar type for OApp publishing
Step 1 — Project Setup
mkdir oasis-quest-log && cd oasis-quest-log npm init -y npm install @oasisomniverse/web4-api express
oasis-quest-log/ ├── server.js ← Express server (serves static files + API proxy) ├── public/ │ ├── index.html ← Login page │ ├── dashboard.html ← Player dashboard │ └── app.js ← Frontend SDK usage └── oasis.js ← Shared SDK client
Step 2 — Shared SDK Client
import { OASISClient } from '@oasisomniverse/web4-api'; export const oasis = new OASISClient({ persistSession: true // store JWT in localStorage }); // Karma to award per quest completion export const QUEST_KARMA = 'CompletingQuest'; export const MILESTONE_EVERY = 10; // mint an NFT every 10th quest export const APP_NAME = 'OASIS Quest Log';
Step 3 — Login Page
<form id="loginForm"> <input type="email" id="email" placeholder="Avatar email" required> <input type="password" id="password" placeholder="Password" required> <button type="submit">Log in with OASIS</button> <a href="#" id="registerLink">New to OASIS? Create an account</a> </form> <script type="module"> import { oasis } from './app.js'; // Redirect if already logged in if (oasis.auth.isAuthenticated()) location.href = '/dashboard.html'; document.getElementById('loginForm').addEventListener('submit', async e => { e.preventDefault(); const result = await oasis.auth.login({ username: document.getElementById('email').value, password: document.getElementById('password').value }); if (result.isError) { showError(result.message); } else { location.href = '/dashboard.html'; } }); </script>
Step 4 — Player Dashboard
The dashboard loads the avatar's karma, quest history (stored as Holons) and NFT collection on page load.
import { oasis, QUEST_KARMA, MILESTONE_EVERY, APP_NAME } from '../oasis.js'; export async function loadDashboard() { if (!oasis.auth.isAuthenticated()) { location.href = '/'; return; } const { avatarId, username } = oasis.auth.getSession(); // Parallel fetch — avatar detail, karma stats, quest history const [avatarRes, karmaRes, questRes] = await Promise.all([ oasis.avatar.getAvatarDetail({ id: avatarId }), oasis.karma.getKarmaStats({ avatarId }), oasis.data.loadHolonsByMetaData({ metaData: { appName: APP_NAME, avatarId } }) ]); const avatar = avatarRes.result; const karma = karmaRes.result; const quests = questRes.result ?? []; renderProfile({ username: avatar.username, level: avatar.level, karma: karma.total }); renderQuestList(quests.sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate))); renderMilestoneProgress(quests.length, MILESTONE_EVERY); }
Step 5 — Submit a Quest
When a player logs a completed quest, we: save a Holon, award karma, and check for the milestone NFT.
export async function submitQuest(questData) { const { avatarId } = oasis.auth.getSession(); // 1. Save quest as a Holon const { result: questHolon, isError } = await oasis.data.saveHolon({ name: 'CompletedQuest', description: questData.title, avatarId, metaData: { appName: APP_NAME, avatarId, title: questData.title, description: questData.description, difficulty: questData.difficulty, // 'Easy' | 'Medium' | 'Hard' | 'Legendary' completedAt: new Date().toISOString() } }); if (isError) throw new Error('Failed to save quest'); // 2. Award karma await oasis.karma.addKarmaToAvatar({ avatarId, karmaType: QUEST_KARMA, karmaSource: APP_NAME, karmaSourceId: questHolon.id, karmaSourceTitle: questData.title }); // 3. Count total quests — check milestone const { result: allQuests } = await oasis.data.loadHolonsByMetaData({ metaData: { appName: APP_NAME, avatarId } }); if (allQuests.length % MILESTONE_EVERY === 0) { await mintMilestoneNFT(avatarId, allQuests.length); } return questHolon; } async function mintMilestoneNFT(avatarId, questCount) { const milestone = questCount / MILESTONE_EVERY; const { result: nft } = await oasis.nft.importWeb4NFTAsync( { importedByAvatarId: avatarId }, { title: `Quest Master — Tier ${milestone}`, description: `Awarded for completing ${questCount} quests in ${APP_NAME}`, imageUrl: `https://myapp.com/assets/milestone-${milestone}.png`, attributes: [ { traitType: 'Milestone', value: String(milestone) }, { traitType: 'QuestCount', value: String(questCount) }, { traitType: 'App', value: APP_NAME } ] } ); // Transfer to the player's wallet await oasis.nft.collectNFTAsync({ oasisNFTId: nft.oasisNFTId, avatarId }); console.log(`Milestone NFT minted: ${nft.title}`); showMilestoneToast(nft.title); }
Use Promise.all() for independent operations (like loading avatar detail + karma stats together) to halve your round-trip time. Only use sequential await when one result feeds the next — like saving a Holon then using its ID to award karma.
Step 6 — Express Server
The SDK works entirely from the browser. The Express server just serves the static files and optionally adds a server-side session check.
import express from 'express'; import path from 'path'; const app = express(); app.use(express.static('public')); // SPA fallback — serve index.html for all routes app.get('*', (req, res) => res.sendFile(path.join(process.cwd(), 'public/index.html')) ); app.listen(3000, () => console.log('Quest Log running on http://localhost:3000'));
node server.js Quest Log running on http://localhost:3000
Step 7 — Publish Your OApp
Once your app is working, register it with the OASIS ecosystem so it appears in the OPORTAL OApp directory and users can find it. You need a Wizard avatar type to publish.
import { oasis } from './oasis.js'; const { avatarId } = oasis.auth.getSession(); const { result: oapp } = await oasis.oapp.publishOAPP({ name: 'OASIS Quest Log', description: 'Track your quests, earn karma and collect milestone NFTs.', version: '1.0.0', url: 'https://questlog.myapp.com', category: 'Productivity', tags: ['quests', 'karma', 'nft', 'gaming'], iconUrl: 'https://questlog.myapp.com/icon.png', screenshotUrls: ['https://questlog.myapp.com/ss1.png'], createdByAvatarId: avatarId, openSource: true, sourceCodeUrl: 'https://github.com/myorg/oasis-quest-log' }); console.log('OApp published! ID:', oapp.id); console.log('OPORTAL URL:', `https://oportal.oasisomniverse.one/oapps/${oapp.id}`);
Complete Flow Diagram
1. User visits your app → redirected to OASIS login
2. oasis.auth.login() → JWT stored, redirect to dashboard
3. Dashboard loads avatar, karma stats and quest history in parallel
4. User submits a quest → Holon saved → karma awarded
5. Every 10th quest → NFT minted and transferred to avatar wallet
6. User's karma and NFT are visible in every other OASIS-connected app
An HR team built this exact pattern as an internal recognition tool. Employees log "good deeds" (helping a colleague, mentoring, shipping a feature on time). Each deed earns karma. At karma milestones they receive a physical reward, with the NFT serving as the tamper-proof proof of achievement. Because karma is OASIS-wide, employees who also contribute to OASIS open-source projects get extra karma that counts toward their work rewards — a cross-ecosystem incentive at zero extra integration cost.
Using the OASIS UI Component Libraries
Rather than building login forms and avatar cards from scratch, use the official OASIS Web Component libraries — available for Vanilla JS, React, Angular, Vue, Svelte and Next.js:
# React npm install @oasisomniverse/react-ui-component-library # Vanilla JS Web Components npm install @oasisomniverse/js-ui-component-library
import { OASISLogin, AvatarCard, KarmaBadge } from '@oasisomniverse/react-ui-component-library'; function App() { return ( <div> {/* Drop-in login form with OASIS branding */} <OASISLogin onSuccess={session => redirectToDashboard(session)} /> {/* Avatar profile card */} <AvatarCard avatarId={session.avatarId} showKarma showLevel /> {/* Inline karma badge */} <KarmaBadge avatarId={session.avatarId} /> </div> ); }