Introduction

Universal RPG character registry (via AT Protocol)

rpg.actor is a way for players and masters of various roleplaying game systems to store, update, and validate important aspects of their characters like stats and sprites in a public way that allows for cross-compatible use.


Through it, users may self-store elements central to their roleplaying experiences using the AT Protocol data ecosystem. By signing in with a Bluesky handle (or any other personal data server), one can manage their character sheets and enable a range of interoperable features that allow seamless use across different game systems.

Core Concepts

Every user has access to three main record types that allow for an expansive and interconnected roleplaying experience across any number of potential games, systems, and services. These are their technical definitions:

Record ConceptRecord TypeDescription
Character Statsactor.rpg.statsCharacter sheet data for multiple systems (D&D, DCC, RMMZ, and others)
Character Spritesactor.rpg.spriteCharacter sprite sheet with animation metadata
Sprite Generatoractor.rpg.generatorSeparated sprite layers and configurations for recomposition
Master Validationsactor.rpg.masterGame Master validations linked to player stats
Equipment (Item)equipment.rpg.itemItem records for defining player inventory pieces
Equipment (Give)equipment.rpg.giveAttestation from item providers for source authenticity

Each record type follows a specific lexicon that assures interoperability across different games or services, all held within each user's own Personal Data Server (PDS) and publicly accessible through the AT Protocol.


 Player Characters

Your character sheet, stored in your own data!

You are a character (always have been), and rpg.actor lets you store, update, and share your own stats, sprites, and more in your own personal record that can work across multiple games, regardless which systems you use.


Because these character sheets are stored through the AT Protocol, they are portable and public. This means that you can login to any compatible experience with your existing @actor.handle and continue your adventures or customize to your preferences, with full knowledge of the wizard, warrior, or whatever you really are!


Think of it like a public database of adventurers you can choose to be part of, which can remember the spells, inventory, and more that matter to you, across different kinds of games both online, and offline.

Your Character Stats

Your character details are held in an actor.rpg.stats record, stored in your own Personal Data Server.


This record can contain stat blocks for any number of systems simultaneously, allowing you to have consistent representation across different games. There's native support for popular tabletop systems like Dungeons & Dragons, Cyberpunk 2020, Mage: The Ascension, and Vampire: The Masquerade, as well as digital formats like RPG Maker MZ, or even your own custom system all in one place.


Stats can be edited directly through the rpg.actor interface and save directly to your PDS, updating your stats across any games that use it. Likewise, (if you permit) supporting games can update your progress automatically and feed your character's growing legacy. You are always in control of these records and will always be able to reshape, remove, or rectify them however you see fit. Because your records are sovereign, you retain authority.


"Great!" she says, "I'll make myself an unstoppable LVL 999 Space Wizard!" — Go ahead. It's your character.


Part of what makes the rpg.actor ecosystem amazing is the way that Masters of various games can check and validate your records for their campaigns with their own records, to make sure you fit well in their worlds. 


Ultimately, these are your characters and you will always stay in control, but playing nice with others and building upon shared experiences in cooperative ways is what good roleplaying has always been about.

Your Character Sprite

Each actor can have a sprite associated to it through an actor.rpg.sprite record which can work alongside their character sheets to provide a visual representation in games where that's useful.


Currently, these sprites follow a general standard (based on RPG Maker MZ) for wide compatibility. The default format is a 144 × 192 pixel PNG using a 3-column, 4-row grid, but the schema can support custom dimensions. 


These sprites appear on rpg.actor profiles and are available to use in services that reads from your PDS, meaning that any compatible games can allow you to login with your @actor.handle and instantly appear as you wish!

Source Tracking

The actor.rpg.sprite record includes an optional source field (AT-URI format) that indicates how the sprite was produced. When a sprite is composed through the Sprite Generator, this field is automatically set to the AT-URI of the user's actor.rpg.generator record (e.g. at://did:plc:xxx/actor.rpg.generator/self). Custom-uploaded sprites will not have this field.


This allows services to detect whether a sprite was built from the generator and whether it remains in sync with the underlying layer data, enabling safe recomposition (adding items, changing layers) without overwriting manually uploaded artwork.


Additional generators or other means of sprite sourcing may also leverage this field to establish recomposition methods for different experiences or sprite types.


Whatever sprite you use, be mindful of copyright and ethical use. See our terms of service for the full breakdown.

Sprite Generator

The Sprite Generator allows players to build a character sprite from interchangeable layers (body, hair, eyes, tops, bottoms, accessories, and more). The resulting composite is saved as the player's actor.rpg.sprite record, while the individual layers and configuration are stored separately in an actor.rpg.generator record.

How the Records Pair

These two records work together but are structurally independent:

RecordKeyContainsUsed By
actor.rpg.spriteselfSingle composite PNG blob (144×192), animation metadataFinal renders; profile pages, game characters, etc
actor.rpg.generatorselfIndividual layer PNGs + body type, skin/eye config, items listSprite recomposition; equipment changes, layer alterations

When a sprite is saved through the generator, the .sprite record's source field is set to the AT-URI of the .generator record. This link allows services to safely recompose the sprite (e.g. adding equipment layers) by drawing from the decomposed layer data rather than guessing how to modify the flattened composite.

Generator Record Structure

The actor.rpg.generator record stores everything needed to recreate or modify a sprite without the full generator UI:

  • version — Generator version that produced the record
  • items[] — Simple list of item identifiers (e.g. "popcorn", "atmosphere_shirt") that can gate interactions
  • bodyType — Body type identifier (e.g. "male", "female")
  • skin / eyes — Color configuration (tone, mode, hex values)
  • layers[] — Ordered array of layer objects (back-to-front), each containing:
Layer FieldTypeRequiredDescription
idstringYesLayer category (e.g. "body", "tops", "hair", "righthand")
blobblob (PNG)YesThe recolored layer PNG (144×192)
assetNamestringNoGenerator asset filename (e.g. "001_basic_short")
titlestringNoHuman-readable name
colorsobjectNoApplied color selections: main, sub1, sub2, sub3
colorwayblob (PNG)NoColorway maps are sentinel-colored pixels marking recolorable regions. Stored so recoloring can happen without re-downloading the original asset.
subtractMaskblob (PNG)NoBinary mask PNG. Opaque black pixels erase the composite below this layer before the layer is drawn. Extracted from black regions in the asset's colorway at save time. See Recomposition below.
behindRowsint[]NoSprite-sheet row indices (0–3) where this layer composites behind the entire existing composite (all previously drawn layers) via destination-over blending. Since layers are ordered back-to-front with body at index 0, this effectively places the item behind the body and all clothing/hair/accessories above it. Row 0 = down, 1 = left, 2 = right, 3 = up. Omit for normal source-over on all rows.

Colorway System

Layer assets use a companion colorway map (_c image) — a 144×192 PNG where specific sentinel colors mark regions that can be recolored by the player. The generator identifies these sentinel pixels and replaces them with the player's chosen colors at the corresponding luminance.

Standard Channels

All clothing, headwear, glasses, necklaces, and facial accessories use the same four standardised sentinel colors:

ChannelSentinel ColorHexPurpose
main#0000FF — Pure BluePrimary color region (e.g. shirt fabric, hat body)
sub1#00FF00 — Pure GreenSecondary region (e.g. trim, accents, sleeves)
sub2#FF0000 — Pure RedThird region (e.g. pattern, emblem)
sub3#FFFFFF — Pure WhiteFourth region (e.g. buttons, buckles)

Special Sentinels

SentinelSentinel ColorHexPurpose
Skin#F9C19DReplaced by the player's skin tone gradient. Used on body, hands, and any asset that shows skin.
Eyes#2C80CB or #1380F8Replaced by the player's eye color gradient.
Hair#FCCB0AReplaced by the player's chosen hair color with luminance modulation.
Sub Mask#000000 Pure black pixels in a colorway map define a subtractive mask. These pixels erase the composite below the layer before it is drawn, punching holes through previously rendered layers. Used for items that need to occlude parts of the body (e.g. hair not poking through a hat).

Skin, eyes, and hair sentinels are relevant to body-adjacent assets. Clothing and accessories use the four standard channels. An asset only needs to define channels it actually uses.

Recomposition

To rebuild a sprite from its generator layers, process each layer in order on a 144×192 canvas:

  1. SubtractMask — If the layer has a subtractMask, erase pixels from the current composite wherever the mask has opaque black pixels. This punches holes in layers below before the new layer is drawn. (If behindRows is also present, the mask is only applied on those rows.)
  2. BehindRows — If the layer has behindRows, its pixels on those rows are drawn behind the entire existing composite (destination-over) rather than on top. Since layers are ordered back-to-front with body first, this effectively places the item behind the body and all clothing/accessories above it. On rows not listed in behindRows, the layer draws normally (source-over). This lets held items like popcorn appear behind the character when facing left or up, while remaining visible in front when facing down or right.
  3. Normal Draw — If neither field is present, simply draw the layer on top at its layer level

Layers are pre-positioned and pre-colored, so no additional alignment or palette logic is needed. The resulting composite matches what the generator produces.

Simple recomposition: If you don't need SubtractMask/BehindRows support, drawing each layers[].blob in order with source-over compositing still produces a usable result for most sprites. The advanced fields only matter for items that need to interact with body occlusion.
Custom Sprites: Actors are not required to use the generator. A custom actor.rpg.sprite without a matching .generator record is valid and will display normally everywhere. The generator record is only needed for layer-based features like equipment overlays.

Equipment & Inventory

The equipment system allows providers (game services, experiences, events) to issue items to players as verifiable AT Protocol records. Each item is represented by a paired record model: one record in the player's PDS as a .item with its details, and another in the provider's PDS as a .give attesting its origin.

Paired Record Model

RecordLives OnCreated ByPurpose
equipment.rpg.giveProvider's PDSGame ProviderAttestation that the item was legitimately granted. Contains recipient DID, item ID, asset CID for integrity verification.
equipment.rpg.itemPlayer's PDSPlayer (Game Client)The player's actual item. Contains the asset blob, icon blob, display metadata, and a reference back to the .give URI for verification.

This dual-record design means that neither party can unilaterally fabricate items. The provider's .give record proves the grant happened, while the player's .item record holds the actual asset data. Verification is possible by checking that the .item's give URI points to a valid .give record on the provider's PDS, and that the assetCid matches.

Players are able to generate .item records independently for self-generated user content, but without a matching .give record, these items cannot be source verified and should be treated like general user content.

Item Kinds

Items fall into two kinds, controlled by the kind field:

KindDescriptionExample
layerWearable items that integrate into the sprite generator as visual layers. Have a category mapping to a generator slot.ATmosphere Shirt (tops), Popcorn (righthand)
inventoryNon-wearable items displayed in the player's inventory but not rendered on the sprite.Keys, potions, collectibles

Layer Item Properties

Items with kind: "layer" can carry additional fields that control how they render on the sprite:

FieldTypeDescription
colorwayblob (PNG)Colorway map for the asset with sentinel-colored pixels indicating recolorable regions. Required for items that support player recoloring.
channels[]arrayColor channel definitions declared by this item's colorway. Each entry has a name (e.g. "main", "sub1"), a sentinel color hex, and an optional defaultColor hex.
behindRowsint[]Sprite-sheet row indices (0–3) where this layer composites behind the body. See Recomposition for details.

When a layer item is equipped through the generator, its colorway and behindRows are carried into the .generator layer entry. The generator uses the colorway to apply the player's chosen colors and can derive a subtractMask from black regions in the colorway at save time.

Generator-compatible items: Providing a proper colorway and channels in your .item records makes them fully recolorable inside the Sprite Generator UI. Players can change the item's colors just like any built-in asset, and the generator will handle luminance modulation, subtractive masking, and behindRows compositing automatically. Items without a colorway still work as static overlays, but lose the ability to be personalized.

Multi-Provider Support

Items track their provider DID, allowing multiple independent services to issue equipment to the same player. Player profiles display items grouped by category with attribution showing which provider granted each item (e.g. "Obtained from @rpg.actor" or "Obtained from @vagabond.quest").

Item Lifecycle

  1. Issue: A game provides means for a player to print an equipment.rpg.item record and the provider creates an equipment.rpg.give record on their PDS.
  2. Display: Items can be loaded independently with or without verification using the blob assets within the record, and the CID from the .give prevents tampering.
  3. Destroy: The player can delete their .item record at any time. The provider's .give record is left intact for historical verification, though can never be reconnected because of CID changes.
Asset Integrity: The .item record stores an assetCid that should match the blob CID from the provider's original grant. This allows third-party verification that the player's item asset hasn't been tampered with since issuance.

Becoming a Provider

Any AT Protocol account can act as an equipment provider. There is no registration required — you write records to your own PDS and the compendium indexes them automatically via Jetstream.

1. Create a give record on your PDS

When your game or service grants an item to a player, write an equipment.rpg.give record to your repository:

// Provider writes to their own PDS
await agent.com.atproto.repo.createRecord({
  repo: PROVIDER_DID,             // your DID
  collection: 'equipment.rpg.give',
  record: {
    recipient: 'did:plc:player...', // player's DID
    item: 'healing_potion',         // your item identifier
    title: 'Healing Potion',        // display name
    givenAt: new Date().toISOString(),
    kind: 'inventory',              // 'layer' or 'inventory'
    category: 'righthand',          // generator slot (for layers)
    description: 'Restores 10 HP',  // optional flavour text
    context: 'Dropped by goblin',   // optional origin note
    assetCid: 'bafkrei...'          // optional: CID of asset PNG
  }
})

2. Player accepts the item on their PDS

The player (or your game client on their behalf, with OAuth authorization) writes an equipment.rpg.item record to their repository:

// Player writes to their own PDS
await agent.com.atproto.repo.createRecord({
  repo: PLAYER_DID,                // player's DID
  collection: 'equipment.rpg.item',
  record: {
    item: 'healing_potion',
    title: 'Healing Potion',
    give: 'at://provider-did/equipment.rpg.give/rkey', // AT-URI of the give
    provider: PROVIDER_DID,
    acceptedAt: new Date().toISOString(),
    asset: assetBlobRef,            // optional: uploaded PNG blob
    assetCid: 'bafkrei...'          // optional: match the give's CID
  }
})

3. Compendium indexes automatically

Both records are picked up by the rpg.actor Jetstream consumer and appear in the equipment index. Query them at any time:

GET https://rpg.actor/api/equipment?player=did:plc:player...
GET https://rpg.actor/api/equipment?provider=did:plc:yourprovider...

Required fields

RecordRequiredOptional
equipment.rpg.giverecipient (DID), item, title, givenAtkind, category, description, assetCid, iconCid, stats, context
equipment.rpg.itemitem, title, give (AT-URI), provider (DID), acceptedAtkind, category, description, asset (blob), icon (blob), colorway (blob), channels, behindRows, assetCid, stats, context
No API required: You do not need to use rpg.actor's API to create equipment records. Write directly to your own PDS using standard AT Protocol methods (com.atproto.repo.createRecord / putRecord). The compendium is an indexer, not a gatekeeper — your records, your authority.

 Game Systems

Define stats for your own game — no gatekeeping required

The actor.rpg.stats record uses an open top-level key model. Each game system gets its own key in the record — dnd, dcc, mage, vampire, cyberpunk2020, rmmz, and so on. As a game developer, you create your own key and write whatever stat structure your game needs. Multiple systems coexist in the same record without conflict, and the rpg.actor UI renders any unknown key automatically using the generic sheet renderer.


There is no registration or approval process. Pick a key, write your data, and the ecosystem picks it up.

System Keys

Your system key is a lowercase alphanumeric identifier that becomes the top-level property name in the player's actor.rpg.stats record. Choose something unique to your game:

GameSystem KeyExample
My Cool RPGmycoolrpg{ "mycoolrpg": { "_meta": { "name": "My Cool RPG" }, ... } }
Dungeon Worlddungeonworld{ "dungeonworld": { "_meta": { "name": "Dungeon World" }, ... } }
Frontier Onlinefrontieronline{ "frontieronline": { "_meta": { "name": "Frontier Online" }, ... } }

Key Rules

  • Lowercase alphanumeric only — letters and digits, no hyphens, underscores, or spaces (/^[a-z0-9]+$/)
  • Must not collide with built-in keys — the following are reserved: dnd, dcc, mage, cyberpunk2020, vampire, rmmz, reverie, playtopia, custom
  • Must not collide with record metadata$type, createdAt, updatedAt are off-limits
  • Be specific — prefer your game's actual name over generic terms like rpg or stats
Use _meta.name: Always include a _meta object with a name field in your system data. This is the human-readable display name shown in the rpg.actor UI (tab labels, print headers, etc). Without it, the UI will try to format your key into a title, which may not look the way you want.
Deprecated: singular custom key. Earlier versions of the lexicon defined a single custom property with a { systemName, systemVersion, stats[] } structure. This is still read for backwards compatibility, but new systems should always use their own unique key. The custom key will not be removed, but it is limited to one system and should not be used for new development.

Writing Stats

To write stats for your system, use the standard AT Protocol putRecord flow. The critical rule: always fetch first, merge your key in, then write back. Never blind-write the full record or you'll overwrite the player's other systems.

JavaScript / Node.js

// 1. Fetch the player's existing stats record
const existing = await agent.com.atproto.repo.getRecord({
  repo: playerDid,
  collection: 'actor.rpg.stats',
  rkey: 'self'
}).catch(() => null);

const currentStats = existing?.data?.value || {};

// 2. Merge your system key in
const updated = {
  $type: 'actor.rpg.stats',
  ...currentStats,
  mycoolrpg: {                          // ← your system key
    _meta: { name: 'My Cool RPG' },     // ← display name
    str: 14,
    dex: 12,
    hp: { current: 25, max: 30 },
    class: 'Ranger',
    inventory: ['Longbow', 'Rope'],
    lucky: true
  },
  updatedAt: new Date().toISOString()
};
if (!updated.createdAt) updated.createdAt = updated.updatedAt;

// 3. Write it back
await agent.com.atproto.repo.putRecord({
  repo: playerDid,
  collection: 'actor.rpg.stats',
  rkey: 'self',
  record: updated
});

Python

import requests, json
from datetime import datetime, timezone

pds = "https://player.pds.example"  # player's PDS endpoint
player_did = "did:plc:abc123"

# 1. Fetch existing
try:
    resp = requests.get(f"{pds}/xrpc/com.atproto.repo.getRecord",
        params={"repo": player_did, "collection": "actor.rpg.stats", "rkey": "self"})
    current = resp.json().get("value", {}) if resp.ok else {}
except:
    current = {}

# 2. Merge
now = datetime.now(timezone.utc).isoformat()
current.update({
    "$type": "actor.rpg.stats",
    "mycoolrpg": {
        "_meta": {"name": "My Cool RPG"},
        "str": 14, "dex": 12,
        "hp": {"current": 25, "max": 30},
        "class": "Ranger"
    },
    "updatedAt": now
})
current.setdefault("createdAt", now)

# 3. Write
requests.post(f"{pds}/xrpc/com.atproto.repo.putRecord",
    headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
    json={"repo": player_did, "collection": "actor.rpg.stats",
          "rkey": "self", "record": current})

Godot (GDScript)

# The rpg.actor Godot add-on provides a merge helper:
var my_stats = {
    "_meta": {"name": "My Cool RPG"},
    "str": 14, "dex": 12,
    "hp": {"current": 25, "max": 30}
}
await ATProto.merge_and_put_stats(pds, did, "mycoolrpg", my_stats)
Always Merge: putRecord replaces the entire actor.rpg.stats record. If you skip the fetch step, you will destroy the player's D&D, DCC, Reverie, and any other system data. Fetch → merge → put. Every time.

Data Formats

Your system's stat object is freeform — you can use whatever structure makes sense for your game. The rpg.actor generic renderer understands the following conventions and will display them with appropriate UI controls:

TypeFormatExampleRendered As
Numberinteger"str": 14Compact stat box
Textstring"class": "Ranger"Compact text field (short) or text section (long)
Resource{ current, max }"hp": { "current": 25, "max": 30 }Current/Max bar
Booleanboolean"lucky": trueToggle switch
Liststring[]"inventory": ["Bow", "Rope"]Tag pills
Dropdown{ _select, options[], value }"alignment": { "_select": true, "options": ["Good", "Neutral", "Evil"], "value": "Good" }Select dropdown
Section{ _heading: true }"Combat": { "_heading": true }Section divider
Notelong string"backstory": "Born in..."Full-width text block

Metadata

Include a _meta object at the top level of your system data for display metadata:

"mycoolrpg": {
  "_meta": {
    "name": "My Cool RPG"    // Display name (used in tabs, print headers)
  },
  "str": 14,
  ...
}

Legacy Format

The older { systemName, systemVersion, stats: [{ name, value, min, max }] } array format is still supported for backwards compatibility. The renderer automatically normalises it to flat key-value pairs. New implementations should use the flat format directly.

Print support: All stat types render in the print layout automatically. Numbers and booleans get compact grid cells; long text, notes, and lists get full-width sections. Section headings (_heading) create visual dividers in both the UI and printed sheets.

Reading Stats

To read a player's stats for your system, fetch their actor.rpg.stats record and pull out your key:

// Fetch the full stats record
const res = await fetch(
  `${pds}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=actor.rpg.stats&rkey=self`
);
const record = await res.json();
const myStats = record.value?.mycoolrpg;

if (myStats) {
  console.log('STR:', myStats.str);
  console.log('HP:', myStats.hp?.current, '/', myStats.hp?.max);
} else {
  console.log('Player has no stats for this system');
}

You can also enumerate which systems a player has by checking the top-level keys:

const systems = Object.keys(record.value)
  .filter(k => !['$type', 'createdAt', 'updatedAt'].includes(k));
// → ['dnd', 'mycoolrpg', 'reverie']
The registry API can help: GET /api/actors/full returns cached stats for all actors. For individual lookups, read directly from the player's PDS as shown above — it's always authoritative.

Native Systems

The following game systems have native sheet support on rpg.actor — full lexicon definitions, dedicated UI renderers, and documented field schemas. Each link goes to the full field reference on the Systems page.

SystemKeyDescription
Dungeons & Dragons 5edndThe world's most played tabletop RPG. Classes, levels, spells, and the d20 system.
Dungeon Crawl ClassicsdccOld-school d20 game with a funnel system, mercurial magic, patron bonds, and powerful corruption rules.
Cyberpunk 2020cyberpunk2020R. Talsorian's gritty near-future RPG of chrome, street cred, and humanity cost.
Mage: The AscensionmageWhite Wolf World of Darkness. Reality is subjective; willworkers reshape it through Spheres and Arete.
Vampire: The MasqueradevampireWhite Wolf World of Darkness. Kindred politics, the hunger, and the slow erosion of humanity.
RPG Maker MZrmmzKadokawa's game engine for JRPG-style titles. Stats map directly to the engine's native actor parameters.
PlaytopiaplaytopiaSocial RPG platform with shared world characters and progression.
ReveriereveriePhilosophical alignment system built around paired axes of ideological disposition.
Custom (legacy)customSingle-slot free-form system record. Deprecated — new integrations should use a unique top-level key.

For any system not listed above, the rpg.actor UI will render it automatically using the generic sheet renderer — no native support required.


 Dungeon Masters

Player-controlled characters, Master-authorized validations

Players are capable of hosting their own character sheets through an actor.rpg.stats records. Nobody but the player themselves can edit these records, which is a core concept behind the autonomous nature of this system.


Masters however can validate the sheets using an actor.rpg.master record that references the user specifically, and declares the whether or not the stats are valid within the context of their campaign. This can be done either manually or systematically, allowing for verification of player records and decentralized agreement for stats.


This dual-record system allows for characters and worlds to be managed autonomously by those who should be in control of them, while still allowing for assured compatibility and approval within worlds that scale. Whether your campaign has two characters or two thousand, the validation records can track and manage fair play for all.

Validation Types

Different campaigns require different levels of authority, and the .master records can accommodate varying levels of trust / strictness to adhere the validation. Whether you want to build a rigid tournament system with zero editing tolerance, or an open free-form experience with no cares at all, both can be managed while keeping coherent reference to which players a Master considers worthy of connective play in their worlds.

Validation strictness is controlled by the snapshotScope field in each actor.rpg.master record. This field determines how the player's live stats are compared against the master's approved snapshot:

TypesnapshotScopeUse CaseBehavior Mode
Inherent Trust none Narrative games Always accepting, regardless of player edits. No stats snapshot stored.
Custom Confidence custom Most campaigns Validates only selected fields. The stats field holds a partial snapshot of approved values.
Hard Record full (default) Tournament play Any player-made change breaks validation. Full stats snapshot is stored and compared exactly.

Validation records may also hold a campaign name to keep players organized across different groupings, and a spriteCid to approve a specific sprite blob alongside the stats.

Master's Protip: Using custom scope and validating combat-relevant groups (abilities, spell lists, and saves) while leaving frequently edited elements like personality or hit points can build a system with very little authorization oversight to manage.

Staying in Sync

When a validations match a player's current stats, their sheet will show "Approved by: @master.handle" to link them with the approving Master. If the player edits something to break validation, the approval disappears until the records are re-validated, or the player reverts their changes to the approved state.


Players can use the Check Masters button to see every validation that references their character, and quickly adopt the stats professed by the .master record as they wish. This is handy for easy recovery, campaigns with controlled levelling, or for switching states between different session configurations dictated by the Masters.


 RPG Maker

Introducing living NPCs and persistent characters

With rpg.actor, players can log into your RPG Maker game with their AT Protocol handle and bring their character along, complete with stats and sprite. The data lives in their own AT Protocol repository, so there's no server to run and no save files to manage. A player's progress can follow them between games automatically.


The system works through keyed RPG Maker MZ values within their actor.rpg.stats record that can persist outside your game files, and follow the player into other experiences. By authenticating into your game via OAuth 2.0 (Authorization Code + PKCE + DPoP), players grant scoped access to their actor.rpg.stats, actor.rpg.sprite, and actor.rpg.master records. Credentials are ephemeral — held only in memory for the duration of the session, never written to save files or disk. If a session expires mid-game, the plugin can prompt a seamless re-authentication without losing progress.


Beyond player characters, the plugin suite enables living NPC populations drawn from real AT Protocol users on the rpg.actor registry. Wandering NPCs can appear organically in your world using region-based spawning, while important characters can be pinned to specific map events by DID. Every NPC carries their real profile, sprite, and stats — and players can even invite them into their party.

MZ Plugins

The rpg.actor plugin set for RPG Maker MZ handles AT Protocol authentication, stats synchronization, sprite loading, NPC spawning, and social post integration. Drop the plugins into your project and players can connect their characters immediately.

RPG Maker MZ Plugins

Compatible with RPG Maker MZ supporting login and data read/write

Download .zip v0.18.1
Jam Sample Project

Example project with OAuth login and read/write for stats and sprites

Download .zip v0.16

The Jam Sample Project is a complete RPG Maker MZ project demonstrating plugin setup, login, stat sync, NPC spawning, and post integration.

Plugin Load Order

PluginRequiredPurpose
RPGACTOR_CoreYesAT Protocol core — auth, XRPC, identity, party, game variables
RPGACTOR_LoginYesAT Protocol OAuth login with profile preview, reauth on session expiry
RPGACTOR_NPCOptionalAT Protocol NPCs — registration, region spawning, wanderers, dialogue
RPGACTOR_PostsOptionalPost viewer, compose window, and social escape codes
RPGACTOR_SpritesOptionalSprite pipeline — PDS records, character selector, face generation
RPGACTOR_StatsOptionalStat sync — actor.rpg.stats PDS records and rpg.actor master records
Load Order Matters: RPGACTOR_Core must be loaded first in the Plugin Manager. RPGACTOR_Login should follow immediately after Core. The remaining plugins (NPC, Posts, Sprites, Stats) can be loaded in any order after Login, though NPC depends on Sprites and Stats at runtime for full functionality.

Authentication

The plugin suite uses OAuth 2.0 Authorization Code flow with PKCE and DPoP for secure authentication. This is the same standard used by Bluesky's first-party applications.

How It Works

  1. The player enters their Bluesky handle in the login overlay
  2. The plugin resolves their PDS endpoint and fetches the OAuth authorization server metadata
  3. A Pushed Authorization Request (PAR) is sent with a PKCE code challenge
  4. The player's default browser opens to the authorization page on their PDS
  5. After approval, a localhost callback server captures the authorization code
  6. The code is exchanged for DPoP-bound access and refresh tokens

Ephemeral credentials: Tokens are held in memory only (RpgActor._tempCredentials). Nothing is written to localStorage, save files, or disk. When the game closes, credentials are gone. Save files store only the player's DID and handle for identity continuity — not tokens.

Session Lifecycle

  • Access tokens auto-refresh every 30 minutes in the background
  • If a session expires and reauthOnLoad is enabled, the login overlay reappears with the handle pre-filled and read-only
  • The player can also continue offline — their local game state is preserved
Desktop Requirement: OAuth requires NW.js (the standard RPG Maker MZ desktop runtime) to spin up the localhost callback server and access Node.js crypto APIs. Browser/web deploys are not currently supported for authenticated flows.

Scopes Requested

atproto
repo:actor.rpg.stats
repo:actor.rpg.sprite
repo:actor.rpg.master
blob:image/*

These scopes allow the game to read and write the player's character stats, sprite, and master validation records, plus upload sprite images as blobs.

Stats & Sprites

Each player can store values relevant to your RPG Maker MZ games through the dedicated rmmz key inside their actor.rpg.stats record. These map directly to the standard in-engine actor parameters:

StatWhat It StoresExample
levelCharacter level5
classClass name"Warrior"
xpTotal experience1250
hp / maxHpCurrent and max hit points38 / 45
mp / maxMpCurrent and max magic points12 / 20
atk, def, mat, mdf, agi, lukCore parameters18, 14, 8, 10, 12, 9
hit, eva, criRates (stored as 0–100)95, 5, 4
tp / maxTpCurrent and max tactical points50 / 100

Sprites are also stored as standard RMMZ character sheets (144×192 PNG, 48×48 frames, 3×4 grid) through player actor.rpg.sprite records. These can be downloaded live from their PDS and load directly into your game for either main characters, or connected NPCs that can be updated from beyond the confines of update patches.


When a player levels up or their stats change in your game, those changes can be pushed back to their personal records so long as the player authorizes the game to do so, allowing other compatible games to keep continuity.

Multi-engine Preservation: When the plugin writes to a player's actor.rpg.stats record, it only touches the rmmz key. Any other system sections in the record (D&D, DCC, Reverie, custom systems, etc.) are preserved. The plugin always fetches the full record first, merges the RMMZ data in, then writes the whole record back.
First-login Snapshot: If a player has no existing actor.rpg.stats record when they log in, the plugin automatically snapshots the current in-game actor's stats and uploads them as the initial PDS record.

Avatar-to-Face Pipeline

The player's AT Protocol avatar is automatically downloaded and converted into an RMMZ-compatible face graphic (scaled to 144×144 in a 576×288 face sheet). This means every authenticated player gets a face graphic in dialogue windows without any manual setup.

Sprite Naming Convention

Custom PDS sprites are saved to img/characters/ using the naming pattern $bsky_handle (the $ prefix tells RMMZ it's a single-character sheet).

NOTE: The plugin suite is under active development. While the core authentication, stats sync, and sprite pipeline are stable, the NPC and social features are still evolving. Save/load behavior with dynamic NPCs and party members should be tested thoroughly in your specific game configuration. Always merge-write PDS records (fetch, modify, put) rather than blind overwrites to preserve cross-system data.

NPC System

The RPGACTOR_NPC plugin turns real rpg.actor users into living NPCs in your game world. There are three ways NPCs can appear:

Registered NPCs

Manually registered by handle via plugin command. These are persistent across saves and can be spawned onto any map programmatically.

Wandering NPCs

Randomly selected from the rpg.actor registry and placed in designated map regions. Each wanderer carries their real AT Protocol profile, PDS sprite, and stats. They appear organically and can be refreshed each time the player enters a map.

Pinned NPCs

Tied to specific map events using a note tag in the RPG Maker editor:

<rpgactor_pin:did:plc:xxxxxxxxxxxxx>

When the map loads, the plugin resolves the DID, downloads their sprite from PDS, and applies it to the event automatically. This lets you place important rpg.actor characters at fixed locations in your game.

NPC Dialogue & Party Joining

NPCs generate contextual dialogue from their AT Protocol profile (display name, bio, follower count). When talking to an NPC, the player is offered the choice to invite them into the party. Accepting triggers an async pipeline that downloads the NPC's sprite, stats, and avatar, then adds them as a fully functional party member with RMMZ-compatible stats applied.

Party Deduplication: NPCs who are already in the player's party will not respawn as wanderers or registered NPCs, preventing duplicates.

Plugin Commands

FeaturePlugin Command
Register a persistent NPCregisterNpc (handle)
Spawn a registered NPCspawnNpc (handle)
Search & add NPCs via overlayshowNpcLookup
Populate wanderers by regionspawnWanderers (regions, count, requireSprite)
Clear wanderer NPCsclearWanderers

Region-based spawning uses RMMZ map regions (paint regions in the tilemap editor) to control where NPCs can appear. The spawnRegionId parameter sets the default NPC region, while wandererRegions accepts a comma-separated list of region IDs for wanderer placement.

Key Parameters

Each plugin exposes configuration through the RPG Maker MZ Plugin Manager. Below are the most important parameters developers should know about:

RPGACTOR_Core

ParameterDefaultPurpose
mainActorId1Which database actor represents the player character
startVariableId1First of 20 consecutive game variables populated with player profile data
staticActorCount4Actor IDs above this are used for dynamic party members (NPC joins)

RPGACTOR_Login

ParameterDefaultPurpose
showOnNewGametrueAutomatically show login on new game
reauthOnLoadtruePrompt re-login when session expires on save load
allowLoginSkipfalseIf true, players can skip login entirely (not just go offline)
clientIdhttp://localhostOAuth client identifier (loopback for desktop, hosted URL for web)
continueCommonEvent0Common event to auto-run after successful login

RPGACTOR_NPC

ParameterDefaultPurpose
spawnRegionId2Map region ID for registered NPC spawning
wandererRegions3,4Comma-separated region IDs for wanderer placement
wandererCount2How many wanderers to spawn per region
mustHaveSpritetrueOnly spawn wanderers who have a PDS sprite record

Game Variables

The Core plugin populates 20 consecutive game variables (starting from startVariableId) with data from the player's AT Protocol profile, making them available to RMMZ event conditions, text codes, and conditional branches:

OffsetVariableContent
+0HANDLEPlayer's @handle
+1DISPLAY_NAMEDisplay name
+2AVATAR_URLAvatar image URL
+3DIDDecentralized identifier
+4LOGIN_STATUS1 = logged in, 0 = offline
+5FOLLOWERSFollower count
+6FOLLOWINGFollowing count
+7POSTS_COUNTPost count
+8DESCRIPTIONProfile bio
+9BANNER_URLBanner image URL
+10CREATED_ATAccount creation date
+11FACE_GRAPHICGenerated face graphic name
+12ACCOUNT_AGEAccount age metric
+13LISTS_COUNTList count
+14HAS_AVATARBoolean (0/1)
+15HAS_BANNERBoolean (0/1)
+16IS_VERIFIEDBoolean (0/1)
+17LABELSAccount labels
+18PINNED_POSTPinned post reference
+19POST_INDEXCurrent post index

Social Escape Codes

Provided by RPGACTOR_Posts, these can be used in any RMMZ Show Text command to embed live Bluesky data:

CodeReturns
\BSKYPOST[n]Text of cached post #n
\BSKYLIKE[n]Like count
\BSKYRT[n]Repost count
\BSKYRPLY[n]Reply count
\BSKYAUTH[n]Author handle
\BSKYDATE[n]Post date

 Godot Engine

AT Protocol integration for Godot games

The godot-rpg-actor add-on brings full AT Protocol and rpg.actor integration to the Godot Engine. Players can log in with their Bluesky handle via OAuth, and your game can read and write character stats, sprites, and equipment records directly from their personal data server.


The add-on provides four autoload singletons that handle identity resolution, authenticated XRPC calls, the OAuth login flow, and the full rpg.actor API — plus custom Sprite2D and Sprite3D nodes for rendering character sprites from PDS records.


Written entirely in GDScript, it works with Godot 4.x desktop builds (OAuth requires the ability to open a browser and receive a localhost callback).

Explore Repo: godot-rpg-actor

Built and maintained by @techtastic1 (aka @godotguy.rpg.actor) with support for OAuth logins, rpg.actor APIs, and data read/write for Godot 4.x.

View on GitHub →

Add-on Setup

The add-on installs as a standard Godot plugin. When enabled, it registers four autoload singletons that become available globally throughout your project:

AutoloadPurpose
XRPCLow-level HTTP and XRPC request handling with DPoP support, blob uploads, and response parsing
ATProtoAT Protocol identity resolution (handle → DID → PDS) and record operations (get, put, list, merge)
ATProtoOAuthOAuth 2.0 login with PKCE and DPoP — browser-based auth, token management, and auto-refresh
RpgActorrpg.actor API wrapper — actors, search, masters, sprites, equipment, and creator endpoints

Installation

  1. Clone or download the repository from GitHub
  2. Copy the addons/rpg_actor/ folder into your project's addons/ directory
  3. In the Godot editor, go to Project → Project Settings → Plugins and enable rpg.actor
  4. The four autoloads are registered automatically when the plugin is enabled
Load Order: The plugin registers autoloads in dependency order: XRPC first (used by all others), then RpgActor, ATProto, and ATProtoOAuth. You do not need to configure this manually.

Authentication

The ATProtoOAuth singleton implements OAuth 2.0 Authorization Code flow with PKCE and DPoP — the same standard used by the RPG Maker plugins and Bluesky's own applications.

How It Works

  1. Call ATProtoOAuth.login("handle.bsky.social")
  2. The add-on resolves the handle to a DID and PDS endpoint
  3. OAuth metadata is fetched from the PDS authorization server
  4. A PKCE challenge and DPoP keypair are generated
  5. A Pushed Authorization Request (PAR) is sent if supported
  6. The player's browser opens to authorize
  7. A localhost TCP server captures the callback
  8. The authorization code is exchanged for DPoP-bound tokens
# Basic login flow
var result = await ATProtoOAuth.login("yourname.bsky.social")
if result.success:
    print("Logged in as: ", result.did)
    print("Handle: ", result.handle)
else:
    print("Login failed: ", result.error)

Signals

SignalParametersEmitted When
login_completedsuccess: bool, did: StringOAuth flow completes (success or failure)
login_failederror: StringAny step of the login flow fails
logout_completedSession is cleared via logout()

Session Management

  • Tokens are held in memory only — nothing written to disk
  • Access tokens auto-refresh every 25 minutes via a background timer
  • Use ATProtoOAuth.is_authenticated() to check session state
  • Call ATProtoOAuth.logout() to clear all credentials
  • Session accessors: get_session_did(), get_session_handle(), get_user_pds()

Scopes Requested

atproto
repo:actor.rpg.stats
repo:actor.rpg.sprite
repo:actor.rpg.master
repo:equipment.rpg.item
repo:equipment.rpg.give
blob:image/*

These scopes allow reading and writing character stats, sprites, master validations, and equipment records, plus uploading image blobs.

Desktop Requirement: OAuth requires a desktop build to spin up the localhost TCP callback server. The add-on opens the system browser for authorization and listens on a configurable local port (default 7000) for the redirect.

Records & Stats

The ATProto singleton provides direct access to AT Protocol record operations. These work with any collection, including all actor.rpg.* and equipment.rpg.* records.

Reading Records

# Resolve a handle to DID + PDS
var identity = await ATProto.resolve_handle("player.bsky.social")
var did = identity.did
var pds = identity.pds

# Fetch character stats
var stats = await ATProto.get_record(pds, did, "actor.rpg.stats")

# Fetch sprite record
var sprite = await ATProto.get_record(pds, did, "actor.rpg.sprite")

# List equipment items
var items = await ATProto.list_records(pds, did, "equipment.rpg.item")

Writing Records (Merge-Safe)

The add-on includes a merge_and_put_stats helper that fetches the existing record, merges your system key in, and writes it back — preserving data from other systems:

# Write Godot-specific stats without overwriting other system data
var pds = ATProtoOAuth.get_user_pds()
var did = ATProtoOAuth.get_session_did()

var my_stats = {
    "level": 5,
    "class": "Ranger",
    "hp": 42,
    "maxHp": 50
}

# Only touches the "godot" key; D&D, RMMZ, etc. are preserved
await ATProto.merge_and_put_stats(pds, did, "godot", my_stats)
Always Merge: putRecord replaces the entire record. If you write stats directly without fetching first, you will overwrite data from other systems. Use merge_and_put_stats() or implement your own fetch-merge-put cycle.

rpg.actor API

The RpgActor singleton wraps the rpg.actor REST API for convenience:

# Search the registry
var results = await RpgActor.search_actors("ranger")

# Get all actors (or full data)
var actors = await RpgActor.get_actors()
var full = await RpgActor.get_actors(true)

# Get a normalized sprite as PNG bytes
var sprite_bytes = await RpgActor.get_sprite(did)

# Query equipment
var player_items = await RpgActor.get_equipment_by_player(did)
var provider_gives = await RpgActor.get_equipment_by_provider(provider_did)

# Master validations
var masters = await RpgActor.get_masters_for_player(did)

Blob Uploads

Upload images to the player's PDS for sprite or equipment asset records:

var png_data: PackedByteArray = image.save_png_to_buffer()
var blob_ref = await XRPC.upload_blob(pds, png_data, "image/png")

Sprite Nodes

The add-on includes custom RpgActorSprite2D and RpgActorSprite3D nodes that extend Godot's built-in sprite nodes. These can load character sprites directly from an actor's PDS record, making it easy to display rpg.actor characters in both 2D and 3D scenes.

NodeExtendsUse Case
RpgActorSprite2DSprite2D2D games, UI character displays, side-scrollers
RpgActorSprite3DSprite3D3D worlds with billboard sprites, mixed 2D/3D

Sprites follow the standard rpg.actor format (144×192 PNG, 3-column × 4-row grid of 48×48 frames). The normalized sprite endpoint serves this format for any registered actor.

Project Settings

The plugin registers the following project settings automatically when enabled. These can be adjusted in Project → Project Settings:

SettingDefaultPurpose
rpg_actor/apihttps://rpg.actor/apirpg.actor API base URL
bluesky/api/publichttps://public.api.bsky.appBluesky public API endpoint
bluesky/api/authhttps://bsky.socialBluesky auth API endpoint
atproto/plc_directoryhttps://plc.directoryPLC directory for DID resolution
atproto/oauth/client_id_urlhttp://localhostOAuth client identifier
atproto/oauth/local_callback_port7000Localhost port for OAuth callback server
atproto/oauth/scope(see below)OAuth scopes requested during login
Callback Port: If port 7000 is busy, the add-on will automatically try a random available port. For most setups, the default works fine.

 Creator System

Build rosters, manage characters, hand them off to players

The Creator System lets game masters, developers, and storytellers create and manage entire rosters of character identities on rpg.actor. Each character is a real AT Protocol account with its own unique name.rpg.actor handle, character sheet, sprite, and full federated identity.


Creator accounts are available in two tiers through a one-time purchase — no subscription required:

TierCharacter SlotsFeatures
Builder10Full character creation, roster management, handoff
Master50Everything in Builder + bulk operations, custom domains

Additional character slots can be purchased at any time. Every character you create appears on the registry alongside everyone else and is fully compatible with Bluesky and other AT Protocol applications.

Characters & Roster

From the Creator Panel, you can create new characters that are instantly provisioned as real PDS accounts. Each character gets:

  • A unique name.rpg.actor handle (or a custom domain handle)
  • Their own actor.rpg.stats and actor.rpg.sprite records
  • A full AT Protocol identity discoverable across the network
  • Optional campaign tagging for organizing characters by game or story

You can switch into any character to edit their sheet and sprite directly, clone characters to create variations, rename handles, and delete characters you no longer need. The Creator Panel also supports writing actor.rpg.master validation records for your characters programmatically.

Handoff & Adoption

Characters you create can be handed off to real players, transferring full account ownership. There are two handoff methods:

  • Email handoff — Send an invitation email with a secure claim link. The recipient sets their own password and takes ownership.
  • Link handoff — Generate a claim link to share directly. Anyone with the link can claim the character.

When a player claims a character, they can either keep it as a standalone account (retaining the name.rpg.actor handle) or adopt the character data into their existing Bluesky/AT Protocol account, merging the stats and sprite into their own repository.


After handoff, the character slot frees up for you to create a new character. If a player later deletes their adopted character data, you can reclaim the handle to reuse it.

Custom Domains

Creator accounts can register custom domains so that characters use handles like hero.yourgame.com instead of hero.rpg.actor. This is especially useful for game developers who want characters to carry their game's branding.

Setup Instructions

  1. Add a wildcard CNAME record for your domain pointing to rpg.actor:
    *.yourgame.com  CNAME  rpg.actor.
    This allows any name.yourgame.com subdomain to route to the rpg.actor server.
  2. Register the domain in your Creator Panel under the Custom Domains section. The system will verify DNS resolution and provision TLS certificates automatically via Caddy.
  3. Create characters using the domain. When creating or renaming a character, select your domain from the dropdown to assign handles like hero.yourgame.com.
DNS Propagation: After adding the CNAME record, DNS changes can take up to 24 hours to propagate. The verification step will check for correct resolution before allowing characters to use the domain. If verification fails, you can retry after propagation completes.
No Conflicting A Records: If the domain has existing A or AAAA records for the wildcard subdomain, they will conflict with the CNAME. Remove any conflicting records before adding the CNAME to rpg.actor.

 ATproto

You can just build things!

Everything on rpg.actor is built upon a set of open lexicons. These schema definitions describe how character data is structured inside user's AT Protocol repositories. The registry indexes and displays these records, and any ATproto-enabled service can read or write the same data.

Character Records

LexiconKeyPurpose
actor.rpg.statsselfCharacter stats supporting multiple systems per record (D&D, DCC, RMMZ, and others)
actor.rpg.spriteselfStandardized sprite sheet in PNG + animation metadata
actor.rpg.generatorselfSeparated sprite layers and configuration for recomposition
actor.rpg.masterTIDMaster validation records referencing a player's stats

Equipment Records

LexiconKeyRepoPurpose
equipment.rpg.giveanyProvider's PDSAttestation that a provider granted an item to a player
equipment.rpg.itemanyPlayer's PDSPlayer-held item record with asset blob and display metadata

The raw schemas for each lexicon are available here:

Changing Schema: These lexicons are actively evolving. New system keys and fields may be added in future revisions, but we have designed for forward compatibility through highly optional fielding. Implement only as you see fit.

API Access

Public endpoints for querying the registry, rate limited to 150 requests per minute per IP. These are organized by scope below.

Registry (Public)

EndpointDescription
GET /api/actorsAll indexed actor DIDs with collection metadata (hasSprite, hasStats, timestamps)
GET /api/actors/fullFull cached actor data for all actors (profiles, sprites, stats)
GET /api/search?q=...Search actors by handle or display name (min 2 chars, max 10 results)
GET /api/statsRegistry summary metrics (actor count, system count, master authority count)
GET /api/healthService health, actor/master counts, and uptime

Masters (Public)

EndpointDescription
GET /api/masters?player=did:...Master validations for a specific player (add &verify=true to check live)
GET /api/masters/by-authority?authority=did:...All players validated by a specific master/GM

Sprites & Media (Public)

EndpointDescription
GET /api/sprite/normalized?did=...Standard 144×192 PNG sprite sheet for any actor
GET /api/og/image?id=...Generated 1200×630 Open Graph card image (sprite + stats)

Equipment (Public)

EndpointDescription
GET /api/equipmentEquipment index summary — total gives, items, providers, and known provider list
GET /api/equipment?player=did:...All items owned by a player and all gives addressed to them, across all providers
GET /api/equipment?provider=did:...All gives issued by a specific provider

The compendium indexes equipment.rpg.give and equipment.rpg.item records from across the AT Protocol via Jetstream. Any provider or player writing such records to a PDS will appear in this index automatically.

Creator (Public)

EndpointDescription
GET /api/creator/pricingCurrent pricing tiers, early bird availability, and PDS status
GET /api/creator/check?did=...Check if a DID is a registered creator account

Account (Authenticated)

EndpointDescription
POST /api/pds-loginPassword login for rpg.actor native accounts
POST /api/account/update-emailUpdate PDS account email (requires DID + current password)
POST /api/account/change-passwordChange account password (requires DID + current + new password)
POST /api/refresh-actorTrigger immediate cache refresh after a stat or sprite mutation
POST /api/contactContact form submission
Build on your own foundation: These API endpoints are provided as a convenience, but we may change rate limits or functionality over time. If you're building something serious, you're better off running your own Jetstream consumer to monitor actor.rpg.* records directly. That way you own your data pipeline and don't depend on us!

Working with Records

Every player's data lives in their own Personal Data Server. Reading it is a straightforward getRecord call via reference through the user's DID and declaration of which collection you're after:

GET {pds}/xrpc/com.atproto.repo.getRecord
  ?repo=did:plc:...
  &collection=actor.rpg.stats
  &rkey=self

The user's DID can typically be resolved through their @actor.handle through the plc.directory, which will also provide you their PDS endpoint. With that information, their actor.rpg.* records can be easily located.


Writing follows the same idea, only requiring authentication. Any service that secures user authorization through either OAuth or an app password can then use putRecord to add or update actor.rpg.* records.


Important Notice: putRecord replaces the whole record. If a player has .stats for multiple systems and you do not fetch to merge changes within it, you will only write part of their record and other systems may disappear in the overwrite. Always fetch first, merge your changes in, then save the whole thing back.


The AT Protocol Lexicon Guide covers the rest of general work with open schema in detail.