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

terminal
mkdir oasis-quest-log && cd oasis-quest-log
npm init -y
npm install @oasisomniverse/web4-api express
project structure
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

oasis.js
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

public/index.html (key section)
<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.

public/app.js
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.

public/app.js (continued)
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);
}
💡
Parallel API calls

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.

server.js
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'));
terminal
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.

publish-oapp.js
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

🔄
User flow summary

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

🌍
Real-world use case — employee recognition platform

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:

terminal
# React
npm install @oasisomniverse/react-ui-component-library

# Vanilla JS Web Components
npm install @oasisomniverse/js-ui-component-library
jsx (React example)
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>
  );
}