Tekton Nakama Lua Server API Reference
Auto-generated from source code. For AI agents — call these RPCs without reading Lua.
Table of Contents
- Authentication & Core (core.lua)
- Utilities (utils.lua)
- User Module (user.lua)
- Economy Module (economy.lua)
- Gacha Module (gacha.lua)
- Leaderboard Module (leaderboard.lua)
- Inbox/Mail Module (inbox.lua)
- Daily Rewards Module (daily_rewards.lua)
- Admin Module (admin.lua)
- Storage Collections Reference
- Leaderboard Config
- Error Strings Reference
- Permission Levels
1. Authentication & Core (core.lua)
After-Hook: after_authenticate_steam
| Field |
Value |
| Trigger |
AuthenticateSteam |
| Purpose |
On first Steam login: sets display_name from Steam username, default role to "player" |
| Auth |
None (internal hook) |
Behavior:
- If user has no
display_name, sets it from input.username (fallback: "SteamPlayer")
- If user metadata has no
role, sets metadata.role = "player"
Startup (not an RPC)
Creates native leaderboard "global_high_score" on module load (also created in leaderboard.lua — pcall wraps both so duplicate is safe).
2. Utilities (utils.lua)
Shared helpers used by other modules.
Constants
| Constant |
Value |
Purpose |
ADMIN_ROLES |
{admin=true, moderator=true, owner=true} |
Roles with admin privileges |
SYSTEM_USER_ID |
"00000000-0000-0000-0000-000000000000" |
System/nil user for global storage |
CHANNEL_TYPE_ROOM |
1 |
Nakama channel type for rooms |
CHANNEL_TYPE_DIRECT |
2 |
Nakama channel type for DMs |
CHANNEL_TYPE_GROUP |
3 |
Nakama channel type for groups |
utils.is_admin(context)
Checks if caller has admin/moderator/owner role.
| Param |
Type |
Description |
context |
table |
Nakama RPC context |
Returns: boolean
utils.is_match_host(context, match_id)
Checks if caller is the host of a match.
| Param |
Type |
Description |
context |
table |
Nakama RPC context |
match_id |
string |
Match ID to check |
Returns: boolean
Note: Reads match.state and checks state.hostUserId against context.user_id.
utils.require_admin(context)
Errors with "Admin privileges required" if caller is not admin/moderator/owner.
utils.require_admin_or_host(context, match_id)
Errors with "Admin or host privileges required" if caller is neither admin nor match host.
utils.resolve_channel_id(channel_id)
Resolves a friendly room name (e.g. "social_global") to a hashed Nakama channel ID.
| Param |
Type |
Description |
channel_id |
string |
Raw channel ID or friendly room name |
Returns: string — resolved channel ID (passes through if already hashed, i.e. contains ".")
Logic: If no "." in value, calls nk.channel_id_build("", name, CHANNEL_TYPE_ROOM).
3. User Module (user.lua)
RPC: get_user_profile
| Field |
Value |
| Function |
user.rpc_get_user_profile |
| Auth |
None required (can view any profile) |
| Purpose |
Get a user's public profile |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
user_id |
string |
No |
context.user_id |
Target user ID |
Returns (JSON):
| Field |
Type |
Description |
user_id |
string |
User ID |
username |
string |
Username |
display_name |
string |
Display name |
avatar_url |
string |
Avatar URL |
create_time |
string |
Account creation time |
role |
string |
Role from metadata ("player" default) |
Errors:
"Account not found" — user doesn't exist
"Account banned until <time>. Reason: <reason>" — temporary ban still active
"Account permanently banned. Reason: <reason>" — permanent ban
Ban logic: Reads metadata.banned, metadata.ban_expires (Unix timestamp), metadata.ban_reason. If ban_expires <= os.time(), auto-clears the ban on read.
RPC: update_user_profile
| Field |
Value |
| Function |
user.rpc_update_user_profile |
| Auth |
Required |
| Purpose |
Update own display name and/or avatar |
Params (JSON):
| Field |
Type |
Required |
Description |
display_name |
string |
No |
New display name |
avatar_url |
string |
No |
New avatar URL |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
Always true |
Errors:
"Not authenticated"
"Failed to update profile"
RPC: search_users
| Field |
Value |
| Function |
user.rpc_search_users |
| Auth |
Required |
| Purpose |
Search users by username or display name |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
query |
string |
No |
"" |
Search term (ILike match on username/display_name). Empty = list all (limit 100). |
Returns (JSON):
| Field |
Type |
Description |
users |
array |
Array of user objects |
User object:
| Field |
Type |
Description |
user_id |
string |
User ID |
username |
string |
Username |
display_name |
string |
Display name |
avatar_url |
string |
Avatar URL from metadata |
SQL: Queries users table with ILIKE. Excludes system user 00000000-0000-0000-0000-000000000000. Limit 100.
RPC: change_credentials
| Field |
Value |
| Function |
user.rpc_change_credentials |
| Auth |
Required |
| Purpose |
Change email + password credentials |
Params (JSON):
| Field |
Type |
Required |
Description |
new_email |
string |
No |
New email |
new_password |
string |
No |
New password |
current_password |
string |
If email set |
Current password (required to unlink old email) |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
Always true |
Errors:
"Not authenticated"
"Current password required" — account has email but no current_password provided
"Incorrect current password." — email auth re-verify failed
"Failed to set new credentials: <err>" — link_email failed (rolls back old link)
Logic: Verifies current password via nk.authenticate_email, unlinks old email, links new email+password. On failure, re-links old credentials.
RPC: send_lobby_invite
| Field |
Value |
| Function |
user.rpc_send_lobby_invite |
| Auth |
Required |
| Purpose |
Send a lobby invitation notification |
Params (JSON):
| Field |
Type |
Required |
Description |
to_user_id |
string |
Yes |
Recipient user ID |
match_id |
string |
Yes |
Match/lobby ID to invite to |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
Always true |
Errors:
"Not authenticated"
"Missing to_user_id or match_id"
Notification: Code 1001, persistent (true). Content: {match_id, from_name}.
RPC: send_friend_request
| Field |
Value |
| Function |
user.rpc_send_friend_request |
| Auth |
Required |
| Purpose |
Send a friend request notification |
Params (JSON):
| Field |
Type |
Required |
Description |
user_id |
string |
Yes |
Target user ID |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
Always true |
Errors:
"Not authenticated"
"user_id is required"
"Cannot add yourself"
Notification: Code 1002, persistent (true). Content: {from_user_id, from_name}.
RPC: admin_get_user_history
| Field |
Value |
| Function |
user.rpc_admin_get_user_history |
| Auth |
Admin required |
| Purpose |
Get wallet ledger, login history, and match history for a user |
Params (JSON):
| Field |
Type |
Required |
Description |
user_id |
string |
Yes |
Target user ID |
Returns (JSON):
| Field |
Type |
Description |
history.wallet_ledger |
array |
Last 50 wallet ledger entries |
history.logins |
array |
Login history (up to 20) |
history.matches |
array |
Match history from storage collection "matches" |
Errors:
"Admin privileges required" (via utils.require_admin)
"user_id is required"
After-Hook: after_authenticate
| Field |
Value |
| Trigger |
AuthenticateDevice, AuthenticateEmail, AuthenticateCustom |
| Purpose |
Record login timestamp + IP to user storage |
Storage write: Collection "history", key "logins", permission_read=0, permission_write=0. Keeps last 20 entries. Each entry: {time, ip}.
4. Economy Module (economy.lua)
Shop Catalog
Hardcoded items available for purchase:
| ID |
Name |
Category |
Gold |
Star |
Rarity |
Character |
oldpop-blue-hat |
Oldpop Blue Hat |
head |
100 |
0 |
Common |
Oldpop |
oldpop-green-hat |
Oldpop Green Hat |
head |
100 |
0 |
Common |
Oldpop |
oldpop-red-hat |
Oldpop Red Hat |
head |
100 |
0 |
Common |
Oldpop |
oldpop-yellow-hat |
Oldpop Yellow Hat |
head |
100 |
0 |
Common |
Oldpop |
oldpop-og-pant |
Copper OG Pant |
costume |
0 |
0 |
Common |
Oldpop |
oldpop-grey-pant |
Copper Grey Pant |
costume |
150 |
0 |
Common |
Oldpop |
oldpop-red-pant |
Copper Red Pant |
costume |
150 |
0 |
Common |
Oldpop |
oldpop-yellow-pant |
Copper Yellow Pant |
costume |
150 |
0 |
Common |
Oldpop |
oldpop-blue-gloves |
Oldpop Blue Gloves |
glove |
75 |
0 |
Common |
Oldpop |
oldpop-green-gloves |
Oldpop Green Gloves |
glove |
75 |
0 |
Common |
Oldpop |
oldpop-red-gloves |
Oldpop Red Gloves |
glove |
75 |
0 |
Common |
Oldpop |
oldpop-yellow-gloves |
Oldpop Yellow Gloves |
glove |
75 |
0 |
Common |
Oldpop |
RPC: get_shop_catalog
| Field |
Value |
| Function |
economy.rpc_get_shop_catalog |
| Auth |
Required |
| Purpose |
Get the shop catalog grouped by category, plus featured banners |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
catalog |
object |
Grouped by category (head, costume, glove). Each entry: {id, name, gold, star, rarity, character} |
featured_banners |
array |
Banners from shop_config/featured_banners (max 3 items, each: {item_id, ...}) |
Errors: "Not authenticated"
RPC: buy_currency
| Field |
Value |
| Function |
economy.rpc_buy_currency |
| Auth |
Required |
| Purpose |
Purchase a currency package (IAP) |
Params (JSON):
| Field |
Type |
Required |
Description |
package_id |
string |
Yes |
One of: gold_100, gold_500, gold_1000, gold_2000, gold_5000, gold_10000, star_100, star_250, star_600 |
receipt |
string |
For gold packages |
Store receipt for validation |
idempotency_key |
string |
Yes |
Idempotency key (prevents double-claim) |
store_type |
string |
No |
"google", "apple", or "test" (default: "test") |
Package table:
| Package ID |
Gold Change |
Star Change |
Requires Verification |
gold_100 |
+100 |
0 |
Yes |
gold_500 |
+550 |
0 |
Yes |
gold_1000 |
+1150 |
0 |
Yes |
gold_2000 |
+2400 |
0 |
Yes |
gold_5000 |
+6250 |
0 |
Yes |
gold_10000 |
+13000 |
0 |
Yes |
star_100 |
-500 |
+100 |
No |
star_250 |
-1100 |
+250 |
No |
star_600 |
-2500 |
+600 |
No |
Returns (JSON) — success:
| Field |
Type |
Description |
success |
bool |
true |
status |
string |
"verified" (receipt validated) or "pending" (no receipt, awaiting verification) |
package_id |
string |
Package ID |
duplicate |
bool |
(optional) true if idempotency key already used |
Errors:
"Not authenticated"
"Package ID required"
"Idempotency key required"
"Invalid package ID"
"InvalidReceipt" — IAP validation failed
"NotEnoughFunds" — wallet update failed (e.g. insufficient gold for star packages)
Idempotency: Checks receipts collection with idempotency_key. Returns existing result on duplicate.
Receipt validation: For store_type="test", accepts receipt == "mock_receipt_for_now".
RPC: purchase_item
| Field |
Value |
| Function |
economy.rpc_purchase_item |
| Auth |
Required |
| Purpose |
Purchase an item from the shop catalog |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
item_id |
string |
Yes |
— |
Item ID from catalog |
quantity |
number |
No |
1 |
Quantity (min 1) |
idempotency_key |
string |
Yes |
— |
Idempotency key |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
item |
string |
Item ID purchased |
duplicate |
bool |
(optional) true if idempotency key already used |
Errors:
"Not authenticated"
"Item ID required"
"Invalid quantity"
"Idempotency key required"
"ItemNotFound"
"NotEnoughFunds" — insufficient gold/star balance
"PurchaseFailed" — storage write failed
Storage writes:
- Collection
"inventory", key item_id: {category, purchased_at, quantity}
- Collection
"receipts", key idempotencyKey: {type="item", item_id, quantity, cost, processed_at}
RPC: admin_set_featured_banners
| Field |
Value |
| Function |
economy.rpc_admin_set_featured_banners |
| Auth |
Admin required |
| Purpose |
Set up to 3 featured banner items for the shop |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
banners |
array |
No |
[] |
Array of banner objects, each with {item_id, ...} (max 3) |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
banners |
array |
Final banners array (max 3) |
Errors:
"Admin privileges required"
"Item not found in catalog: <itemId>" — banner references a non-catalog item
Storage: Collection "shop_config", key "featured_banners", user SYSTEM_USER_ID, permission_read=2, permission_write=0.
RPC: admin_get_featured_banners
| Field |
Value |
| Function |
economy.rpc_admin_get_featured_banners |
| Auth |
Admin required |
| Purpose |
Get current featured banners config |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
banners |
array |
Current banners (may be empty) |
5. Gacha Module (gacha.lua)
Gacha Banners
| Banner ID |
Name |
Currency |
Pull 1 Cost |
Pull 10 Cost |
Pity At |
star |
Star Banner |
star |
160 |
1440 |
90 |
gold |
Gold Banner |
gold |
50 |
450 |
90 |
Drop Rates
| Rarity |
Probability |
Pool Items |
real_prize |
1.0% |
skin_gacha_rainbow_suit, skin_gacha_dragon_hat, skin_gacha_phantom_gloves, skin_gacha_neon_acc |
rare |
14.0% |
frag_rare |
uncommon |
25.0% |
frag_uncommon |
common |
60.0% |
frag_common |
Real Prize Catalog
| Item ID |
Name |
Category |
skin_gacha_rainbow_suit |
Rainbow Suit |
costume |
skin_gacha_dragon_hat |
Dragon Hat |
head |
skin_gacha_phantom_gloves |
Phantom Gloves |
glove |
skin_gacha_neon_acc |
Neon Accessory |
accessory |
Fragment Items
| ID |
Name |
Rarity |
frag_common |
Common Fragment |
common |
frag_uncommon |
Uncommon Fragment |
uncommon |
frag_rare |
Rare Fragment |
rare |
RPC: perform_gacha_pull
| Field |
Value |
| Function |
gacha.rpc_perform_gacha_pull |
| Auth |
Required |
| Purpose |
Perform 1 or 10 gacha pulls on a banner |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
banner_id |
string |
Yes |
— |
"star" or "gold" |
count |
number |
No |
1 |
Number of pulls (1 or 10) |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
Always true |
results |
array |
Array of pull results: {id, rarity, name} |
new_pity |
number |
Updated pity counter for this banner |
Errors:
"Not authenticated"
"Banner ID required"
"Invalid count"
"Unknown banner: <banner_id>"
"Could not read account"
"Insufficient currency"
"Failed to update wallet"
"Failed to write storage: <err>"
Pity system: After pity_at (90) pulls without a real_prize, the next pull is guaranteed real_prize. Pity counter resets on real_prize drop. Pity counter persists per banner in storage.
Storage reads/writes:
- Collection
"profiles", key "pity_counters": per-banner pity counts
- Collection
"profiles", key "fragments": fragment inventory
- Collection
"inventory", key = item_id: real prize items
Wallet: Deducts currency cost. For count=10, uses pull_10_cost if available, otherwise pull_1_cost * count.
6. Leaderboard Module (leaderboard.lua)
Leaderboard Config
| Property |
Value |
| ID |
"global_high_score" |
| Authoritative |
true |
| Sort Order |
"desc" (highest first) |
| Operator |
"best" (best score kept) |
| Reset Schedule |
nil (never resets) |
RPC: get_leaderboard_stats
| Field |
Value |
| Function |
leaderboard.rpc_get_leaderboard_stats |
| Auth |
None |
| Purpose |
Get top 50 leaderboard entries |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
leaderboard |
array |
Top 50 leaderboard entries |
Leaderboard entry object:
| Field |
Type |
Description |
user_id |
string |
Owner user ID |
username |
string |
Username |
display_name |
string |
Username (fallback — actually record.username) |
avatar_url |
string |
Avatar URL from metadata |
loadout_character |
string |
Character from metadata (default "Copper") |
high_score |
number |
Score |
games_played |
number |
From metadata |
games_won |
number |
From metadata |
RPC: submit_score
| Field |
Value |
| Function |
leaderboard.rpc_submit_score |
| Auth |
Required |
| Purpose |
Submit/update own score on the leaderboard |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
score |
number |
No |
0 |
Score to submit |
games_played |
number |
No |
0 |
Games played count |
games_won |
number |
No |
0 |
Games won count |
avatar_url |
string |
No |
From account |
Avatar URL |
loadout_character |
string |
No |
"Copper" |
Character selection |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
Errors:
"Not authenticated"
"Failed to submit score"
RPC: sync_leaderboard
| Field |
Value |
| Function |
leaderboard.rpc_sync_leaderboard |
| Auth |
Required |
| Purpose |
Bulk-sync all users' stats from "stats" and "profiles" collections to the native leaderboard |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
Always true |
synced |
number |
Number of records written |
objects_found |
number |
Stats objects found |
debug |
array |
Error messages per user |
Errors:
"Not authenticated"
"Sync failed: <err>"
Storage reads: Collection "stats" (all users, limit 100), Collection "profiles" (all users, limit 100), key "profile".
RPC: reset_stats
| Field |
Value |
| Function |
leaderboard.rpc_reset_stats |
| Auth |
Required |
| Purpose |
Delete own leaderboard record and reset stats to zero |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
Storage write: Collection "stats", key "game_stats" — zeros out games_played, games_won, high_score, total_kills, total_deaths. permission_read=2, permission_write=1.
RPC: admin_update_stats
| Field |
Value |
| Function |
leaderboard.rpc_admin_update_stats |
| Auth |
Admin required |
| Purpose |
Force-overwrite a user's stats and leaderboard record |
Params (JSON):
| Field |
Type |
Required |
Description |
user_id |
string |
Yes |
Target user ID |
stats |
object |
Yes |
Stats object: {high_score, games_played, games_won, loadout_character, ...} |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
Errors:
"Admin privileges required"
"User ID and stats are required"
Storage write: Collection "stats", key "game_stats", permission_read=1, permission_write=0.
RPC: admin_delete_stats
| Field |
Value |
| Function |
leaderboard.rpc_admin_delete_stats |
| Auth |
Admin required |
| Purpose |
Delete a user's stats storage and leaderboard record |
Params (JSON):
| Field |
Type |
Required |
Description |
user_id |
string |
Yes |
Target user ID |
Storage deletes: {collection="stats", key="stats"}, {collection="stats", key="game_stats"}, plus leaderboard record.
RPC: admin_sync_leaderboard
| Field |
Value |
| Function |
leaderboard.rpc_admin_sync_leaderboard |
| Auth |
Admin required |
| Purpose |
Same as sync_leaderboard but requires admin |
Delegates to: leaderboard.rpc_sync_leaderboard (same params, same returns).
7. Inbox/Mail Module (inbox.lua)
RPC: admin_send_mail
| Field |
Value |
| Function |
inbox.rpc_admin_send_mail |
| Auth |
Admin required |
| Purpose |
Send a personal (targeted) or global mail |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
title |
string |
No |
"Announcement" |
Mail title |
content |
string |
No |
"" |
Mail body |
start_date |
string |
No |
Current UTC |
ISO8601 start date |
end_date |
string |
No |
"" |
ISO8601 end date (global only) |
rewards |
array/object |
No |
{} |
Attached rewards (see claim_mail_reward for format) |
target_user_id |
string |
No |
"" |
If set → personal mail to this user. Omit → global mail |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
mail |
object |
The created mail object |
Mail object fields: id (UUIDv4), title, content, sender ("TEKTON DEV TEAM"), date, start_date, end_date, expiry_date (30 days from now), rewards, type ("personal" or "global").
Personal storage: Collection "inbox", key "personal", permission_read=1, permission_write=0.
Global storage: Collection "config", key "global_mail", user SYSTEM_USER_ID, permission_read=2, permission_write=0.
RPC: get_mail
| Field |
Value |
| Function |
inbox.rpc_get_mail |
| Auth |
Required |
| Purpose |
Get all non-deleted, non-expired personal + global mail for the user |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
mails |
array |
Filtered mail objects (excludes deleted, expired, not-yet-started, ended global) |
state |
object |
{claimed_ids, deleted_ids, read_ids} |
Filtering logic: Excludes mail where:
- ID is in
state.deleted_ids
expiry_date is in the past
start_date is in the future
- Global mail with
end_date in the past
Storage reads:
- Collection
"inbox", key "personal", user = caller
- Collection
"config", key "global_mail", user = SYSTEM_USER_ID
- Collection
"inbox", key "state", user = caller
RPC: claim_mail_reward
| Field |
Value |
| Function |
inbox.rpc_claim_mail_reward |
| Auth |
Required |
| Purpose |
Claim rewards attached to a mail |
Params (JSON):
| Field |
Type |
Required |
Description |
mail_id |
string |
Yes |
Mail ID to claim rewards from |
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
claimed_ids |
array |
Updated list of claimed mail IDs |
Reward format (in the mail object):
Legacy dict format: {star=100, gold=50}
Array format: [{type="star", amount=100}, {type="gold", amount=50}, {type="frag_rare", id="frag_rare", amount=1}, {type="skin", id="skin_gacha_rainbow_suit"}]
Reward type |
Behavior |
"star" |
Added to wallet |
"gold" |
Added to wallet |
"frag_" prefix |
Added to inventory fragments (key: "fragments") |
"item" |
Added to inventory fragments |
"skin" |
Added to inventory (key = skin item ID) |
Errors:
"Not authenticated"
"mail_id required"
"Reward already claimed"
"Mail not found"
Storage writes: Wallet update, then collection "inventory" (fragments or skins), then "inbox"/"state".
RPC: delete_mail
| Field |
Value |
| Function |
inbox.rpc_delete_mail |
| Auth |
Required |
| Purpose |
Soft-delete a mail (adds ID to state.deleted_ids) |
Params (JSON):
| Field |
Type |
Required |
Description |
mail_id |
string |
Yes |
Mail ID to delete |
RPC: save_mail_state
| Field |
Value |
| Function |
inbox.rpc_save_mail_state |
| Auth |
Required |
| Purpose |
Mark mails as read |
Params (JSON):
| Field |
Type |
Required |
Description |
read_ids |
array |
No |
Array of mail IDs to mark as read |
RPC: admin_list_mail
| Field |
Value |
| Function |
inbox.rpc_admin_list_mail |
| Auth |
Admin required |
| Purpose |
List all mail (global + personal) across all users |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
mails |
array |
All mails sorted by date descending. Personal mails have extra fields: type="personal", target_user_id |
RPC: admin_update_mail
| Field |
Value |
| Function |
inbox.rpc_admin_update_mail |
| Auth |
Admin required |
| Purpose |
Edit or move a mail between users |
Params (JSON):
| Field |
Type |
Required |
Description |
mail_id |
string |
Yes |
Mail ID to update |
type |
string |
No |
"global" or "personal" |
target_user_id |
string |
If personal |
Current owner (for lookup) |
new_target_user_id |
string |
No |
Transfer to a different user |
title |
string |
No |
New title |
content |
string |
No |
New content |
end_date |
string |
No |
New end date |
expiry_date |
string |
No |
New expiry date |
RPC: admin_delete_mail_server
| Field |
Value |
| Function |
inbox.rpc_admin_delete_mail_server |
| Auth |
Admin required |
| Purpose |
Permanently delete mail from server storage |
Params (JSON):
| Field |
Type |
Required |
Description |
mail_id |
string |
Yes |
Mail ID to delete |
type |
string |
No |
"global" or "personal" |
target_user_id |
string |
If personal |
Owner user ID |
8. Daily Rewards Module (daily_rewards.lua)
Default Reward Schedule
Days 1–31, each day gives star currency: min(10 + (day-1)*5, 100).
| Day |
Reward |
| 1 |
10 star |
| 2 |
15 star |
| ... |
... |
| 19 |
100 star (capped) |
| 20–31 |
100 star each |
RPC: claim_daily_reward
| Field |
Value |
| Function |
daily_rewards.rpc_claim_daily_reward |
| Auth |
Required |
| Purpose |
Claim today's daily reward |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
success |
bool |
true |
reward_type |
string |
"star", "gold", or a fragment ID (starts with "frag_") |
reward_amount |
number |
Amount rewarded |
day |
number |
Day number (1-indexed) |
Errors:
"Not authenticated"
"Already claimed today" — last_claim_date == today
"Already claimed all rewards for this month" — day index >= rewards array length
"Already claimed today's reward" — day index already in claimed_days
Storage reads/writes:
- Read: Collection
"daily_rewards", key "state" (per-user)
- Read: Collection
"config", key "daily_rewards", user SYSTEM_USER_ID (config)
- Write: Collection
"daily_rewards", key "state" (updates claimed days)
- Write (if fragment): Collection
"inventory", key "fragments"
- Wallet update for star/gold rewards
Reward types: "star" or "gold" → wallet update. "frag_" prefix → inventory fragments.
RPC: get_daily_reward_state
| Field |
Value |
| Function |
daily_rewards.rpc_get_daily_reward_state |
| Auth |
Required |
| Purpose |
Get current daily reward state and month config |
Params: None (ignore payload)
Returns (JSON):
| Field |
Type |
Description |
state |
object |
{claimed_days, last_claim_date, month} |
month_rewards |
array |
Reward config for current month (array of {type, amount}) |
can_claim_today |
bool |
Whether user can claim today |
today_date |
string |
Today's UTC date (YYYY-MM-DD) |
today_index |
number |
Today's 0-based index |
server_month |
number |
Current month number |
RPC: set_daily_reward_config [Admin]
| Field |
Value |
| Function |
daily_rewards.rpc_set_daily_reward_config |
| Auth |
Admin required |
| Purpose |
Set monthly daily reward configuration |
Params (JSON):
| Field |
Type |
Required |
Description |
config |
object |
Yes |
Map of month → array of {type, amount} rewards. E.g. {"01": [{type="star", amount=50}, ...]} |
RPC: get_daily_reward_config_admin [Admin]
| Field |
Value |
| Function |
daily_rewards.rpc_get_daily_reward_config_admin |
| Auth |
Admin required |
| Purpose |
Get the current daily reward config |
9. Admin Module (admin.lua)
RPC: admin_kick_player
| Field |
Value |
| Function |
admin.rpc_admin_kick_player |
| Auth |
Admin or match host |
| Purpose |
Kick a player from a match |
Params (JSON):
| Field |
Type |
Required |
Description |
match_id |
string |
Yes |
Match ID |
user_id |
string |
Yes |
Player to kick |
reason |
string |
No |
"Kicked by admin" |
Errors:
"Admin or host privileges required"
"Cannot kick yourself"
"Failed to kick player"
Mechanism: Sends nk.match_signal with {action="kick", user_id, reason}.
RPC: admin_ban_player
| Field |
Value |
| Function |
admin.rpc_admin_ban_player |
| Auth |
Admin required |
| Purpose |
Ban a player (sets metadata.banned, kicks from match) |
Params (JSON):
| Field |
Type |
Required |
Description |
user_id |
string |
Yes |
User to ban |
reason |
string |
No |
"Banned by admin" |
duration_hours |
number |
No |
Hours until ban expires (omitted = permanent) |
match_id |
string |
No |
Also kick from this match |
Errors:
"Admin privileges required"
"Cannot ban yourself"
"Target account not found"
"Cannot ban an admin"
Storage: Writes ban record to collection "bans", key = user_id, user = SYSTEM_USER_ID, permission_read=2, permission_write=0.
RPC: admin_unban_player
| Field |
Value |
| Function |
admin.rpc_admin_unban_player |
| Auth |
Admin required |
| Purpose |
Remove ban from a player |
Params (JSON):
| Field |
Type |
Required |
user_id |
string |
Yes |
RPC: admin_get_ban_list
| Field |
Value |
| Function |
admin.rpc_admin_get_ban_list |
| Auth |
Admin required |
| Purpose |
List all ban records |
RPC: admin_get_server_stats
| Field |
Value |
| Function |
admin.rpc_admin_get_server_stats |
| Auth |
Admin (or host if match_id provided) |
| Purpose |
Get server stats (active matches, players) |
Params (JSON):
| Field |
Type |
Required |
Description |
match_id |
string |
No |
If set, includes match-specific stats |
Returns (JSON):
| Field |
Type |
Description |
active_matches |
number |
Count of active matches |
total_players |
number |
Total players in matches |
server_time |
number |
os.time() |
match |
object |
(optional) {id, size, tick_rate, authoritative} |
RPC: admin_end_match
| Field |
Value |
| Function |
admin.rpc_admin_end_match |
| Auth |
Admin or match host |
| Purpose |
Signal a match to end |
Params (JSON):
| Field |
Type |
Required |
Description |
match_id |
string |
Yes |
Match ID |
reason |
string |
No |
"Ended by admin" |
RPC: admin_set_user_role
| Field |
Value |
| Function |
admin.rpc_admin_set_user_role |
| Auth |
Owner only (role must be "owner") |
| Purpose |
Set a user's role |
Params (JSON):
| Field |
Type |
Required |
Valid Values |
user_id |
string |
Yes |
Target user |
role |
string |
Yes |
"player", "moderator", "admin" |
Errors:
"Only owners can modify user roles"
"Invalid role"
RPC: admin_topup_gold
| Field |
Value |
| Function |
admin.rpc_admin_topup_gold |
| Auth |
Admin required |
| Purpose |
Add 999,999 gold to own wallet (dev quick-fill) |
Wallet update: {gold = 999999}
RPC: admin_clear_global_chat
| Field |
Value |
| Function |
admin.rpc_admin_clear_global_chat |
| Auth |
Admin required |
| Purpose |
Delete all messages from a chat channel |
Params (JSON):
| Field |
Type |
Required |
Description |
channel_id |
string |
Yes |
Channel ID or room name |
Returns (JSON): {success, deleted}
RPC: admin_list_users
| Field |
Value |
| Function |
admin.rpc_admin_list_users |
| Auth |
Admin required |
| Purpose |
List all users (limit 500) with role and ban info |
Returns (JSON): {users: [{user_id, username, display_name, create_time, role, banned, ban_reason}], count}
RPC: admin_delete_users
| Field |
Value |
| Function |
admin.rpc_admin_delete_users |
| Auth |
Admin required |
| Purpose |
Permanently delete user accounts |
Params (JSON):
| Field |
Type |
Required |
Description |
user_ids |
array |
Yes |
Array of user IDs to delete |
Errors:
"Cannot delete your own account"
"Cannot delete admin account" (prevents deleting admin/moderator/owner)
Returns (JSON): {success, deleted: [user_ids], failed: [{user_id, reason}]}
RPC: admin_get_user_detail
| Field |
Value |
| Function |
admin.rpc_admin_get_user_detail |
| Auth |
Admin required |
| Purpose |
Get comprehensive user details (profile, friends, purchases, storage, wallet ledger) |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
user_id |
string |
Yes |
— |
Target user ID |
collections |
array |
No |
["profiles","stats","inventory","receipts","history","matches","inbox"] |
Storage collections to include |
Returns (JSON): Rich object with {user, friends, purchases, wallet_ledger, storage, subscription}.
RPC: admin_update_user_identity
| Field |
Value |
| Function |
admin.rpc_admin_update_user_identity |
| Auth |
Admin required |
| Purpose |
Update a user's identity fields (username, display_name, timezone, location, lang_tag, avatar_url, metadata) |
Params (JSON):
| Field |
Type |
Required |
user_id |
string |
Yes |
username |
string |
No |
display_name |
string |
No |
timezone |
string |
No |
location |
string |
No |
lang_tag |
string |
No |
avatar_url |
string |
No |
metadata |
object |
No (merged into existing metadata) |
RPC: admin_set_user_password
| Field |
Value |
| Function |
admin.rpc_admin_set_user_password |
| Auth |
Admin required |
| Purpose |
Force-set a user's password (user must have email credential) |
Params (JSON):
| Field |
Type |
Required |
user_id |
string |
Yes |
password |
string |
Yes |
Errors:
"User has no email credential"
"user_id and password are required"
RPC: admin_get_player_list
| Field |
Value |
| Function |
admin.rpc_admin_get_player_list |
| Auth |
Admin or match host |
| Purpose |
Get player list for a match (stub — returns empty) |
RPC: admin_get_chat_config
| Field |
Value |
| Function |
admin.rpc_admin_get_chat_config |
| Auth |
Admin required |
| Purpose |
Get lobby chat configuration |
Returns (JSON): {config: {prefix, max_messages, max_age_days}}
RPC: admin_set_chat_config
| Field |
Value |
| Function |
admin.rpc_admin_set_chat_config |
| Auth |
Admin required |
| Purpose |
Set lobby chat configuration |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
prefix |
string |
No |
"" |
Chat prefix |
max_messages |
number |
No |
50 |
Max messages |
max_age_days |
number |
No |
0 |
Max message age |
Storage: Collection "config", key "lobby_chat", user SYSTEM_USER_ID.
RPC: admin_purge_old_messages
| Field |
Value |
| Function |
admin.rpc_admin_purge_old_messages |
| Auth |
Admin required |
| Purpose |
Delete chat messages older than specified days |
Params (JSON):
| Field |
Type |
Required |
Description |
channel_id |
string |
Yes |
Channel ID or room name |
max_age_days |
number |
Yes |
Delete messages older than this |
RPC: admin_list_channel_messages
| Field |
Value |
| Function |
admin.rpc_admin_list_channel_messages |
| Auth |
Admin required |
| Purpose |
List messages in a chat channel |
Params (JSON):
| Field |
Type |
Required |
Default |
Description |
channel_id |
string |
Yes |
— |
Channel ID or room name |
limit |
number |
No |
50 |
Messages per page |
cursor |
string |
No |
"" |
Pagination cursor |
forward |
bool |
No |
true |
Direction |
RPC: admin_delete_channel_message
| Field |
Value |
| Function |
admin.rpc_admin_delete_channel_message |
| Auth |
Admin required |
| Purpose |
Delete a specific chat message |
Params (JSON):
| Field |
Type |
Required |
channel_id |
string |
Yes |
message_id |
string |
Yes |
10. Storage Collections Reference
Collection: "shop_config"
| Key |
User |
Permission Read |
Permission Write |
Schema |
featured_banners |
SYSTEM |
2 (public) |
0 (owner) |
{banners: [{item_id, ...}]} |
Collection: "receipts"
| Key |
User |
Permission Read |
Permission Write |
Schema |
<idempotency_key> |
Owner |
1 (owner-read) |
0 (owner) |
{type, package_id/ item_id, status, changeset, receipt, processed_at, ...} |
Collection: "inventory"
| Key |
User |
Permission Read |
Permission Write |
Schema |
<item_id> |
Owner |
1 (owner-read) |
0 (owner) |
{category, purchased_at, quantity} (or {acquired_via, purchased_at} for skins) |
"fragments" |
Owner |
1 (owner-read) |
0 (owner) |
{frag_common: N, frag_uncommon: N, frag_rare: N, ...} |
Collection: "profiles"
| Key |
User |
Permission Read |
Permission Write |
Schema |
"pity_counters" |
Owner |
1 (owner-read) |
0 (owner) |
{star: N, gold: N} (per-banner pity) |
"fragments" |
Owner |
1 (owner-read) |
0 (owner) |
Fragment counts (same as inventory fragments) |
"profile" |
Owner |
2 (public) |
0 (owner) |
Profile data (avatar_url, loadout_character, etc.) |
Collection: "history"
| Key |
User |
Permission Read |
Permission Write |
Schema |
"logins" |
Owner |
0 (no-read) |
0 (owner) |
{logins: [{time, ip}, ...]} (max 20) |
Collection: "stats"
| Key |
User |
Permission Read |
Permission Write |
Schema |
"game_stats" |
Owner |
1 (owner-read) or 2 (public) |
1 (owner-write) or 0 (owner) |
{games_played, games_won, high_score, total_kills, total_deaths, avatar_url, loadout_character} |
"stats" |
Owner |
Varies |
0 (owner) |
Legacy/user-level stats |
Collection: "inbox"
| Key |
User |
Permission Read |
Permission Write |
Schema |
"personal" |
Owner |
1 (owner-read) |
0 (owner) |
{mails: [{id, title, content, sender, date, start_date, end_date, expiry_date, rewards, type}]} |
"state" |
Owner |
1 (owner-read) |
0 (owner) |
{claimed_ids: [string], deleted_ids: [string], read_ids: [string]} |
Collection: "daily_rewards"
| Key |
User |
Permission Read |
Permission Write |
Schema |
"state" |
Owner |
1 (owner-read) |
0 (owner) |
{claimed_days: [number], last_claim_date: "YYYY-MM-DD", month: "MM"} |
Collection: "config" (system-level)
| Key |
User |
Permission Read |
Permission Write |
Schema |
"daily_rewards" |
SYSTEM |
2 (public) |
0 (owner) |
{"MM": [{type, amount}, ...]} (month→rewards map) |
"global_mail" |
SYSTEM |
2 (public) |
0 (owner) |
{mails: [{id, title, content, ...}]} |
"lobby_chat" |
SYSTEM |
2 (public) |
0 (owner) |
{prefix, max_messages, max_age_days} |
Collection: "bans"
| Key |
User |
Permission Read |
Permission Write |
Schema |
<user_id> |
SYSTEM |
2 (public) |
0 (owner) |
{user_id, username, banned_by, banned_at, reason, expires} |
Collection: "matches" (per-user)
| Key |
User |
Permission |
Schema |
<match_id> |
Owner |
0 (owner) |
Match result data (keyed by match ID, varies) |
11. Leaderboard Config
| Property |
Value |
| ID |
"global_high_score" |
| Authoritative |
true (can only be written server-side) |
| Sort Order |
"desc" (highest → lowest) |
| Operator |
"best" (keep best score per user) |
| Reset |
nil (never auto-resets) |
| Created By |
Both core.lua and leaderboard.lua on module load |
12. Error Strings Reference
Auth errors (raised by utils)
| Error String |
Trigger |
"Admin privileges required" |
Non-admin calls admin RPC |
"Admin or host privileges required" |
Non-admin, non-host calls admin/host RPC |
"Not authenticated" |
context.user_id is nil |
Economy errors
| Error String |
Trigger |
"Package ID required" |
buy_currency called without package_id |
"Idempotency key required" |
Missing idempotency_key |
"Invalid package ID" |
Unknown package_id |
"InvalidReceipt" |
IAP receipt validation failed |
"NotEnoughFunds" |
Insufficient wallet balance for purchase |
"ItemNotFound" |
Item ID not in shop catalog |
"Invalid quantity" |
Quantity < 1 |
"PurchaseFailed" |
Storage write failed during item purchase |
"Item not found in catalog: <id>" |
Admin tried to banner a non-catalog item |
User errors
| Error String |
Trigger |
"Account not found" |
User ID doesn't exist |
"Account banned until <time>. Reason: <reason>" |
Temporary ban |
"Account permanently banned. Reason: <reason>" |
Permanent ban |
"Failed to update profile" |
nk.account_update_id failed |
"Current password required" |
Email change without current password |
"Incorrect current password." |
Email re-auth failed |
"Failed to set new credentials: <err>" |
Link email failed |
"Missing to_user_id or match_id" |
Lobby invite missing fields |
"Cannot add yourself" |
Friend request to self |
"user_id is required" |
Missing target in friend request |
Gacha errors
| Error String |
Trigger |
"Banner ID required" |
Missing banner_id |
"Invalid count" |
Count < 1 |
"Unknown banner: <id>" |
Invalid banner |
"Could not read account" |
Account lookup failed |
"Insufficient currency" |
Not enough star/gold |
"Failed to update wallet" |
Wallet update failed |
"Failed to write storage: <err>" |
Storage write failed |
Leaderboard errors
| Error String |
Trigger |
"Failed to submit score" |
Leaderboard write failed |
"Sync failed: <err>" |
Storage list failed |
"User ID and stats are required" |
Missing params in admin_update_stats |
Inbox errors
| Error String |
Trigger |
"mail_id required" |
Missing mail ID |
"Reward already claimed" |
Mail already in claimed_ids |
"Mail not found" |
Mail ID not in any mailbox |
"Mail not found in global" |
Update/delete: mail not in global |
"Mail not found in personal inbox" |
Update/delete: mail not in personal |
"target_user_id required for personal mail" |
Missing owner for personal mail operations |
Daily reward errors
| Error String |
Trigger |
"Already claimed today" |
last_claim_date == today |
"Already claimed all rewards for this month" |
Day index >= reward array length |
"Already claimed today's reward" |
Day index already in claimed_days |
Admin errors
| Error String |
Trigger |
"Cannot kick yourself" |
Self-kick attempt |
"Failed to kick player" |
Match signal failed |
"Cannot ban yourself" |
Self-ban attempt |
"Target account not found" |
Account lookup failed |
"Cannot ban an admin" |
Target is admin/moderator/owner |
"Only owners can modify user roles" |
Non-owner tries role change |
"Invalid role" |
Role not in {player, moderator, admin} |
"channel_id is required" |
Missing channel ID for chat operations |
"channel_id and message_id are required" |
Missing chat message params |
"Failed to delete message: <err>" |
Channel message remove failed |
"Failed to list messages: <err>" |
Channel list failed |
"max_age_days must be > 0" |
Invalid purge param |
"Match not found" |
Match lookup failed |
"User has no email credential" |
Cannot set password for non-email user |
"Cannot delete your own account" |
Self-deletion attempt |
"Cannot delete admin account" |
Deleting admin/moderator/owner |
"No user IDs provided" |
Empty user_ids array |
13. Permission Levels
Role Hierarchy
| Role |
Can Call Admin RPCs |
Can Modify Roles |
Notes |
"player" |
No |
No |
Default role |
"moderator" |
Yes |
No |
Same as admin for auth checks |
"admin" |
Yes |
No |
Standard admin |
"owner" |
Yes |
Yes (only owner) |
Can set player, moderator, admin roles |
Auth Check Functions Summary
| Check |
What It Allows |
utils.require_admin |
Any RPC gated by this. Roles: admin, moderator, owner |
utils.require_admin_or_host |
Admin/moderator/owner, OR match host (state.hostUserId) |
metadata.role == "owner" |
Only admin_set_user_role |
Storage Permission Bits
| Value |
Meaning |
0 |
Owner only (no one can read/write except server) |
1 |
Owner read, server write |
2 |
Public read, server write |
System User ID
Used for global/shared storage collections (bans, config, global_mail, shop_config).
RPC Summary Table
| RPC Name |
Module |
Auth |
Admin? |
get_user_profile |
user |
None |
No |
update_user_profile |
user |
Required |
No |
search_users |
user |
Required |
No |
change_credentials |
user |
Required |
No |
send_lobby_invite |
user |
Required |
No |
send_friend_request |
user |
Required |
No |
admin_get_user_history |
user |
Required |
Yes |
get_shop_catalog |
economy |
Required |
No |
buy_currency |
economy |
Required |
No |
purchase_item |
economy |
Required |
No |
admin_set_featured_banners |
economy |
Required |
Yes |
admin_get_featured_banners |
economy |
Required |
Yes |
perform_gacha_pull |
gacha |
Required |
No |
get_leaderboard_stats |
leaderboard |
None |
No |
submit_score |
leaderboard |
Required |
No |
sync_leaderboard |
leaderboard |
Required |
No |
reset_stats |
leaderboard |
Required |
No |
admin_update_stats |
leaderboard |
Required |
Yes |
admin_delete_stats |
leaderboard |
Required |
Yes |
admin_sync_leaderboard |
leaderboard |
Required |
Yes |
admin_send_mail |
inbox |
Required |
Yes |
get_mail |
inbox |
Required |
No |
claim_mail_reward |
inbox |
Required |
No |
delete_mail |
inbox |
Required |
No |
save_mail_state |
inbox |
Required |
No |
admin_list_mail |
inbox |
Required |
Yes |
admin_update_mail |
inbox |
Required |
Yes |
admin_delete_mail_server |
inbox |
Required |
Yes |
claim_daily_reward |
daily_rewards |
Required |
No |
get_daily_reward_state |
daily_rewards |
Required |
No |
set_daily_reward_config |
daily_rewards |
Required |
Yes |
get_daily_reward_config_admin |
daily_rewards |
Required |
Yes |
admin_kick_player |
admin |
Required |
Admin or host |
admin_ban_player |
admin |
Required |
Yes |
admin_unban_player |
admin |
Required |
Yes |
admin_get_ban_list |
admin |
Required |
Yes |
admin_get_server_stats |
admin |
Required |
Admin or host |
admin_end_match |
admin |
Required |
Admin or host |
admin_set_user_role |
admin |
Required |
Owner only |
admin_topup_gold |
admin |
Required |
Yes |
admin_clear_global_chat |
admin |
Required |
Yes |
admin_list_users |
admin |
Required |
Yes |
admin_delete_users |
admin |
Required |
Yes |
admin_get_user_detail |
admin |
Required |
Yes |
admin_update_user_identity |
admin |
Required |
Yes |
admin_set_user_password |
admin |
Required |
Yes |
admin_get_player_list |
admin |
Required |
Admin or host |
admin_get_chat_config |
admin |
Required |
Yes |
admin_set_chat_config |
admin |
Required |
Yes |
admin_purge_old_messages |
admin |
Required |
Yes |
admin_list_channel_messages |
admin |
Required |
Yes |
admin_delete_channel_message |
admin |
Required |
Yes |
Wallet Update Summary
| Operation |
Changeset |
Context |
| IAP: gold packages (verified) |
{gold: +N} |
buy_currency |
| IAP: star packages |
{star: +N, gold: -N} |
buy_currency (costs gold) |
| Purchase item |
{gold: -N} or {star: -N} |
purchase_item |
| Gacha pull |
{<currency>: -cost} |
perform_gacha_pull |
| Claim mail rewards |
{star: +N} and/or {gold: +N} |
claim_mail_reward |
| Daily reward |
{star: +N} or {gold: +N} |
claim_daily_reward |
| Admin gold top-up |
{gold: 999999} |
admin_topup_gold |
All wallet updates use nk.wallet_update(user_id, changeset, {}, true) (third arg true = update only, no metadata).