{"_id":"@afuchat/rewards","name":"@afuchat/rewards","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@afuchat/rewards","version":"1.0.0","description":"Zero-config gamification SDK — XP, points, levels, badges, streaks, and leaderboards out of the box.","keywords":["gamification","xp","points","badges","streaks","leaderboard","rewards","sdk","typescript"],"license":"MIT","author":{"name":"AfuChat"},"repository":{"type":"git","url":"git+https://github.com/afuchat/rewards.git"},"homepage":"https://github.com/afuchat/rewards#readme","bugs":{"url":"https://github.com/afuchat/rewards/issues"},"engines":{"node":">=18"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"workspace":"./src/index.ts","types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"typecheck":"tsc -p tsconfig.json --noEmit","build":"node build.mjs && tsc -p tsconfig.build.json","prepublishOnly":"pnpm run build"},"devDependencies":{"@types/node":"catalog:","esbuild":"0.27.3","typescript":"~5.9.3"},"gitHead":"e1289eaa612b8fb836716276fb73195992fac706","_id":"@afuchat/rewards@1.0.0","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-kB6DROAh4lOoXiot8RmHqaebgJZeZ2ryJLm5EzgINJ73omeK2cFg/x6xv+T8AS4PXFR3B97P1YpHoEf0ekLUkg==","shasum":"8cd3836e2e27b13fe581a3f6d3f561d95f150cb7","tarball":"https://registry.npmjs.org/@afuchat/rewards/-/rewards-1.0.0.tgz","fileCount":6,"unpackedSize":101435,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDHTbhGAL0PeMZoNZF42+qfLu5FUQm+yfOvJDC+8vjoagIhAJAXMGRfJbhlQ6A5pJRBOUWUgemAJ+WVyOAyRjyBrt/q"}]},"_npmUser":{"name":"amkaweesi","email":"amkaweesi@afuchat.com"},"directories":{},"maintainers":[{"name":"amkaweesi","email":"amkaweesi@afuchat.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rewards_1.0.0_1780161475059_0.9708702224549066"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-30T17:17:54.926Z","1.0.0":"2026-05-30T17:17:55.194Z","modified":"2026-05-30T17:17:55.423Z"},"maintainers":[{"name":"amkaweesi","email":"amkaweesi@afuchat.com"}],"description":"Zero-config gamification SDK — XP, points, levels, badges, streaks, and leaderboards out of the box.","homepage":"https://github.com/afuchat/rewards#readme","keywords":["gamification","xp","points","badges","streaks","leaderboard","rewards","sdk","typescript"],"repository":{"type":"git","url":"git+https://github.com/afuchat/rewards.git"},"author":{"name":"AfuChat"},"bugs":{"url":"https://github.com/afuchat/rewards/issues"},"license":"MIT","readme":"# @afuchat/rewards\n\nZero-config gamification SDK for Node.js. XP, points, levels, badges, streaks, leaderboards, and a built-in event system — all working instantly after install with no setup required.\n\n## Install\n\n```bash\nnpm install @afuchat/rewards\n```\n\n## Quick Start\n\n```ts\nimport AfuRewards from \"@afuchat/rewards\";\n\nconst rewards = new AfuRewards();\n\nrewards.addXP(\"user1\", 50);\nrewards.unlockBadge(\"user1\", \"Starter\");\n\nconsole.log(rewards.getXP(\"user1\"));    // 50\nconsole.log(rewards.getLevel(\"user1\")); // 1\nconsole.log(rewards.getBadges(\"user1\")); // [{ id: \"Starter\", ... }]\n```\n\nThat's it. No database, no API keys, no configuration.\n\n---\n\n## API Reference\n\n### XP\n\n```ts\nrewards.addXP(userId: string, amount: number): number\nrewards.getXP(userId: string): number\n```\n\n### Points\n\n```ts\nrewards.addPoints(userId: string, amount: number): number\nrewards.deductPoints(userId: string, amount: number): number\nrewards.getPoints(userId: string): number\n```\n\n### Levels\n\nLevel is automatically derived from XP using the default formula:\n`level = floor(sqrt(xp / 100)) + 1`\n\n```ts\nrewards.getLevel(userId: string): number\nrewards.calculateLevel(xp: number): number\n```\n\n**Custom formula:**\n\n```ts\nconst rewards = new AfuRewards({\n  level: {\n    formula: (xp) => Math.floor(xp / 500) + 1,\n  },\n});\n```\n\n### Badges\n\n```ts\n// Optional: pre-define badge metadata\nrewards.defineBadge(\"first_post\", {\n  name: \"First Post\",\n  description: \"Published your first post\",\n  icon: \"📝\",\n});\n\nrewards.unlockBadge(userId: string, badgeId: string): Badge | null\nrewards.getBadges(userId: string): Badge[]\nrewards.hasBadge(userId: string, badgeId: string): boolean\n```\n\n`unlockBadge` returns `null` if the badge was already unlocked. A badge can be\nunlocked without pre-defining it — the `id` is used as the name.\n\n### Streaks\n\n```ts\nrewards.updateStreak(userId: string): StreakRecord  // call once per day\nrewards.getStreak(userId: string): StreakRecord\nrewards.resetStreak(userId: string): void\n```\n\n`StreakRecord`:\n```ts\n{\n  current: number;   // current consecutive days\n  longest: number;   // all-time longest streak\n  lastUpdated: Date | null;\n}\n```\n\nStreak logic:\n- Calling `updateStreak` on the **same day** is a no-op (idempotent).\n- Calling it on the **next consecutive day** increments the streak.\n- Calling it after **missing a day** resets the streak to 1.\n\n### Leaderboard\n\n```ts\nrewards.getLeaderboard(type: \"xp\" | \"points\", limit?: number): LeaderboardEntry[]\n```\n\nReturns users sorted by value descending, with a `rank` field starting at 1.\n\n```ts\nLeaderboardEntry: {\n  userId: string;\n  value: number;\n  rank: number;\n}\n```\n\n### Events\n\nBuilt-in events fire automatically when rewards are granted:\n\n| Event             | Payload                                   |\n|-------------------|-------------------------------------------|\n| `xp_added`        | `{ userId, amount, total }`               |\n| `points_added`    | `{ userId, amount, total }`               |\n| `points_deducted` | `{ userId, amount, total }`               |\n| `badge_unlocked`  | `{ userId, badge }`                       |\n| `streak_updated`  | `{ userId, streak }`                      |\n| `streak_reset`    | `{ userId }`                              |\n\nYou can also fire and listen to **custom events**:\n\n```ts\nrewards.on(\"user_posted\", (payload) => {\n  rewards.addXP(payload.userId, 10);\n  rewards.addPoints(payload.userId, 5);\n});\n\n// Fire from your app logic\nrewards.emit(\"user_posted\", { userId: \"user1\" });\n```\n\n```ts\nrewards.on(eventName: string, callback: (payload: T) => void): void\nrewards.off(eventName: string, callback): void\nrewards.emit(eventName: string, payload: T): void\nrewards.removeAllListeners(eventName?: string): void\n```\n\n---\n\n## Storage\n\nBy default, all data lives in-memory. It resets when the process restarts. Use the persistence API below to save and reload state without any database.\n\n---\n\n## Persistence\n\nSave the entire state to a JSON file and reload it later — no database required.\n\n```ts\n// Save to disk (e.g. on graceful shutdown)\nawait rewards.persist(\"./rewards-state.json\");\n\n// Restore on next startup\nconst rewards = new AfuRewards();\nawait rewards.restore(\"./rewards-state.json\");\n```\n\nFor in-process transfers (e.g. serverless functions sharing state via an external store), use the sync variants:\n\n```ts\n// Get a plain serializable object\nconst snapshot = rewards.snapshot();\n\n// Load it back into any AfuRewards instance\nconst rewards2 = new AfuRewards();\nrewards2.loadSnapshot(snapshot);\n```\n\n### Typical startup pattern\n\n```ts\nimport AfuRewards from \"@afuchat/rewards\";\nimport { existsSync } from \"node:fs\";\n\nconst STATE_FILE = \"./rewards-state.json\";\n\nconst rewards = new AfuRewards();\nif (existsSync(STATE_FILE)) {\n  await rewards.restore(STATE_FILE);\n}\n\n// ... run your app ...\n\n// On shutdown\nprocess.on(\"SIGTERM\", async () => {\n  await rewards.persist(STATE_FILE);\n  process.exit(0);\n});\n```\n\n### Snapshot format\n\nThe snapshot is a plain JSON object with version, timestamp, and all user records:\n\n```json\n{\n  \"version\": 1,\n  \"savedAt\": \"2026-05-30T16:20:01.725Z\",\n  \"users\": {\n    \"alice\": {\n      \"xp\": 300,\n      \"points\": 50,\n      \"badges\": [{ \"id\": \"Starter\", \"name\": \"Starter\", \"description\": \"...\", \"unlockedAt\": \"...\" }],\n      \"streak\": { \"current\": 1, \"longest\": 1, \"lastUpdated\": \"...\" }\n    }\n  }\n}\n```\n\n`restore()` fully **replaces** the current in-memory state (not merges). Date strings are automatically converted back to `Date` objects. An unsupported `version` number throws immediately.\n\n### API\n\n```ts\nrewards.persist(filePath: string): Promise<void>\nrewards.restore(filePath: string): Promise<void>\nrewards.snapshot(): SerializedStore\nrewards.loadSnapshot(snapshot: SerializedStore): void\n```\n\n---\n\n## TypeScript\n\nThe package ships with full type declarations. All types are exported:\n\n```ts\nimport type {\n  Badge,\n  BadgeDefinition,\n  AfuRewardsConfig,\n  LeaderboardEntry,\n  LevelConfig,\n  SerializedStore,\n  StreakRecord,\n} from \"@afuchat/rewards\";\n```\n\n---\n\n## Non-Goals\n\nThis SDK intentionally excludes: payments, wallets, financial rewards, SMS, identity verification, and external APIs.\n","readmeFilename":"README.md","_rev":"1-5280dc231518d1396474be1c16fc6317"}