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 Concept | Record Type | Description |
|---|---|---|
| Character Stats | actor.rpg.stats | Character sheet data for multiple systems (D&D, DCC, RMMZ, and others) |
| Character Sprites | actor.rpg.sprite | Character sprite sheet with animation metadata |
| Sprite Generator | actor.rpg.generator | Separated sprite layers and configurations for recomposition |
| Master Validations | actor.rpg.master | Game Master validations linked to player stats |
| Equipment (Item) | equipment.rpg.item | Item records for defining player inventory pieces |
| Equipment (Give) | equipment.rpg.give | Attestation 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:
| Record | Key | Contains | Used By |
|---|---|---|---|
actor.rpg.sprite | self | Single composite PNG blob (144×192), animation metadata | Final renders; profile pages, game characters, etc |
actor.rpg.generator | self | Individual layer PNGs + body type, skin/eye config, items list | Sprite 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 Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Layer category (e.g. "body", "tops", "hair", "righthand") |
blob | blob (PNG) | Yes | The recolored layer PNG (144×192) |
assetName | string | No | Generator asset filename (e.g. "001_basic_short") |
title | string | No | Human-readable name |
colors | object | No | Applied color selections: main, sub1, sub2, sub3 |
colorway | blob (PNG) | No | Colorway maps are sentinel-colored pixels marking recolorable regions. Stored so recoloring can happen without re-downloading the original asset. |
subtractMask | blob (PNG) | No | Binary 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. |
behindRows | int[] | No | Sprite-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:
| Channel | Sentinel Color | Hex | Purpose |
|---|---|---|---|
main | #0000FF — Pure Blue | Primary color region (e.g. shirt fabric, hat body) | |
sub1 | #00FF00 — Pure Green | Secondary region (e.g. trim, accents, sleeves) | |
sub2 | #FF0000 — Pure Red | Third region (e.g. pattern, emblem) | |
sub3 | #FFFFFF — Pure White | Fourth region (e.g. buttons, buckles) |
Special Sentinels
| Sentinel | Sentinel Color | Hex | Purpose |
|---|---|---|---|
| Skin | #F9C19D | Replaced by the player's skin tone gradient. Used on body, hands, and any asset that shows skin. | |
| Eyes | #2C80CB or #1380F8 | Replaced by the player's eye color gradient. | |
| Hair | #FCCB0A | Replaced 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:
- 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. (IfbehindRowsis also present, the mask is only applied on those rows.) - 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 inbehindRows, 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. - 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.
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.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
| Record | Lives On | Created By | Purpose |
|---|---|---|---|
equipment.rpg.give | Provider's PDS | Game Provider | Attestation that the item was legitimately granted. Contains recipient DID, item ID, asset CID for integrity verification. |
equipment.rpg.item | Player's PDS | Player (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:
| Kind | Description | Example |
|---|---|---|
layer | Wearable items that integrate into the sprite generator as visual layers. Have a category mapping to a generator slot. | ATmosphere Shirt (tops), Popcorn (righthand) |
inventory | Non-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:
| Field | Type | Description |
|---|---|---|
colorway | blob (PNG) | Colorway map for the asset with sentinel-colored pixels indicating recolorable regions. Required for items that support player recoloring. |
channels[] | array | Color 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. |
behindRows | int[] | 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.
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
- Issue: A game provides means for a player to print an
equipment.rpg.itemrecord and the provider creates anequipment.rpg.giverecord on their PDS. - Display: Items can be loaded independently with or without verification using the blob assets within the record, and the CID from the
.giveprevents tampering. - Destroy: The player can delete their
.itemrecord at any time. The provider's.giverecord is left intact for historical verification, though can never be reconnected because of CID changes.
.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
| Record | Required | Optional |
|---|---|---|
equipment.rpg.give | recipient (DID), item, title, givenAt | kind, category, description, assetCid, iconCid, stats, context |
equipment.rpg.item | item, title, give (AT-URI), provider (DID), acceptedAt | kind, category, description, asset (blob), icon (blob), colorway (blob), channels, behindRows, assetCid, stats, context |
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:
| Game | System Key | Example |
|---|---|---|
| My Cool RPG | mycoolrpg | { "mycoolrpg": { "_meta": { "name": "My Cool RPG" }, ... } } |
| Dungeon World | dungeonworld | { "dungeonworld": { "_meta": { "name": "Dungeon World" }, ... } } |
| Frontier Online | frontieronline | { "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,updatedAtare off-limits - Be specific — prefer your game's actual name over generic terms like
rpgorstats
_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.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)
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:
| Type | Format | Example | Rendered As |
|---|---|---|---|
| Number | integer | "str": 14 | Compact stat box |
| Text | string | "class": "Ranger" | Compact text field (short) or text section (long) |
| Resource | { current, max } | "hp": { "current": 25, "max": 30 } | Current/Max bar |
| Boolean | boolean | "lucky": true | Toggle switch |
| List | string[] | "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 |
| Note | long 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.
_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']
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.
| System | Key | Description |
|---|---|---|
| Dungeons & Dragons 5e | dnd | The world's most played tabletop RPG. Classes, levels, spells, and the d20 system. |
| Dungeon Crawl Classics | dcc | Old-school d20 game with a funnel system, mercurial magic, patron bonds, and powerful corruption rules. |
| Cyberpunk 2020 | cyberpunk2020 | R. Talsorian's gritty near-future RPG of chrome, street cred, and humanity cost. |
| Mage: The Ascension | mage | White Wolf World of Darkness. Reality is subjective; willworkers reshape it through Spheres and Arete. |
| Vampire: The Masquerade | vampire | White Wolf World of Darkness. Kindred politics, the hunger, and the slow erosion of humanity. |
| RPG Maker MZ | rmmz | Kadokawa's game engine for JRPG-style titles. Stats map directly to the engine's native actor parameters. |
| Playtopia | playtopia | Social RPG platform with shared world characters and progression. |
| Reverie | reverie | Philosophical alignment system built around paired axes of ideological disposition. |
| Custom (legacy) | custom | Single-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:
| Type | snapshotScope | Use Case | Behavior 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.
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.
Compatible with RPG Maker MZ supporting login and data read/write
Example project with OAuth login and read/write for stats and sprites
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
| Plugin | Required | Purpose |
|---|---|---|
| RPGACTOR_Core | Yes | AT Protocol core — auth, XRPC, identity, party, game variables |
| RPGACTOR_Login | Yes | AT Protocol OAuth login with profile preview, reauth on session expiry |
| RPGACTOR_NPC | Optional | AT Protocol NPCs — registration, region spawning, wanderers, dialogue |
| RPGACTOR_Posts | Optional | Post viewer, compose window, and social escape codes |
| RPGACTOR_Sprites | Optional | Sprite pipeline — PDS records, character selector, face generation |
| RPGACTOR_Stats | Optional | Stat sync — actor.rpg.stats PDS records and rpg.actor master records |
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
- The player enters their Bluesky handle in the login overlay
- The plugin resolves their PDS endpoint and fetches the OAuth authorization server metadata
- A Pushed Authorization Request (PAR) is sent with a PKCE code challenge
- The player's default browser opens to the authorization page on their PDS
- After approval, a localhost callback server captures the authorization code
- 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
reauthOnLoadis 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
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:
| Stat | What It Stores | Example |
|---|---|---|
| level | Character level | 5 |
| class | Class name | "Warrior" |
| xp | Total experience | 1250 |
| hp / maxHp | Current and max hit points | 38 / 45 |
| mp / maxMp | Current and max magic points | 12 / 20 |
| atk, def, mat, mdf, agi, luk | Core parameters | 18, 14, 8, 10, 12, 9 |
| hit, eva, cri | Rates (stored as 0–100) | 95, 5, 4 |
| tp / maxTp | Current and max tactical points | 50 / 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.
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.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.
Plugin Commands
| Feature | Plugin Command |
|---|---|
| Register a persistent NPC | registerNpc (handle) |
| Spawn a registered NPC | spawnNpc (handle) |
| Search & add NPCs via overlay | showNpcLookup |
| Populate wanderers by region | spawnWanderers (regions, count, requireSprite) |
| Clear wanderer NPCs | clearWanderers |
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
| Parameter | Default | Purpose |
|---|---|---|
mainActorId | 1 | Which database actor represents the player character |
startVariableId | 1 | First of 20 consecutive game variables populated with player profile data |
staticActorCount | 4 | Actor IDs above this are used for dynamic party members (NPC joins) |
RPGACTOR_Login
| Parameter | Default | Purpose |
|---|---|---|
showOnNewGame | true | Automatically show login on new game |
reauthOnLoad | true | Prompt re-login when session expires on save load |
allowLoginSkip | false | If true, players can skip login entirely (not just go offline) |
clientId | http://localhost | OAuth client identifier (loopback for desktop, hosted URL for web) |
continueCommonEvent | 0 | Common event to auto-run after successful login |
RPGACTOR_NPC
| Parameter | Default | Purpose |
|---|---|---|
spawnRegionId | 2 | Map region ID for registered NPC spawning |
wandererRegions | 3,4 | Comma-separated region IDs for wanderer placement |
wandererCount | 2 | How many wanderers to spawn per region |
mustHaveSprite | true | Only 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:
| Offset | Variable | Content |
|---|---|---|
| +0 | HANDLE | Player's @handle |
| +1 | DISPLAY_NAME | Display name |
| +2 | AVATAR_URL | Avatar image URL |
| +3 | DID | Decentralized identifier |
| +4 | LOGIN_STATUS | 1 = logged in, 0 = offline |
| +5 | FOLLOWERS | Follower count |
| +6 | FOLLOWING | Following count |
| +7 | POSTS_COUNT | Post count |
| +8 | DESCRIPTION | Profile bio |
| +9 | BANNER_URL | Banner image URL |
| +10 | CREATED_AT | Account creation date |
| +11 | FACE_GRAPHIC | Generated face graphic name |
| +12 | ACCOUNT_AGE | Account age metric |
| +13 | LISTS_COUNT | List count |
| +14 | HAS_AVATAR | Boolean (0/1) |
| +15 | HAS_BANNER | Boolean (0/1) |
| +16 | IS_VERIFIED | Boolean (0/1) |
| +17 | LABELS | Account labels |
| +18 | PINNED_POST | Pinned post reference |
| +19 | POST_INDEX | Current post index |
Social Escape Codes
Provided by RPGACTOR_Posts, these can be used in any RMMZ Show Text command to embed live Bluesky data:
| Code | Returns |
|---|---|
\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).
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.
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:
| Autoload | Purpose |
|---|---|
XRPC | Low-level HTTP and XRPC request handling with DPoP support, blob uploads, and response parsing |
ATProto | AT Protocol identity resolution (handle → DID → PDS) and record operations (get, put, list, merge) |
ATProtoOAuth | OAuth 2.0 login with PKCE and DPoP — browser-based auth, token management, and auto-refresh |
RpgActor | rpg.actor API wrapper — actors, search, masters, sprites, equipment, and creator endpoints |
Installation
- Clone or download the repository from GitHub
- Copy the
addons/rpg_actor/folder into your project'saddons/directory - In the Godot editor, go to Project → Project Settings → Plugins and enable rpg.actor
- The four autoloads are registered automatically when the plugin is enabled
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
- Call
ATProtoOAuth.login("handle.bsky.social") - The add-on resolves the handle to a DID and PDS endpoint
- OAuth metadata is fetched from the PDS authorization server
- A PKCE challenge and DPoP keypair are generated
- A Pushed Authorization Request (PAR) is sent if supported
- The player's browser opens to authorize
- A localhost TCP server captures the callback
- 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
| Signal | Parameters | Emitted When |
|---|---|---|
login_completed | success: bool, did: String | OAuth flow completes (success or failure) |
login_failed | error: String | Any step of the login flow fails |
logout_completed | — | Session 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.
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)
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.
| Node | Extends | Use Case |
|---|---|---|
RpgActorSprite2D | Sprite2D | 2D games, UI character displays, side-scrollers |
RpgActorSprite3D | Sprite3D | 3D 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:
| Setting | Default | Purpose |
|---|---|---|
rpg_actor/api | https://rpg.actor/api | rpg.actor API base URL |
bluesky/api/public | https://public.api.bsky.app | Bluesky public API endpoint |
bluesky/api/auth | https://bsky.social | Bluesky auth API endpoint |
atproto/plc_directory | https://plc.directory | PLC directory for DID resolution |
atproto/oauth/client_id_url | http://localhost | OAuth client identifier |
atproto/oauth/local_callback_port | 7000 | Localhost port for OAuth callback server |
atproto/oauth/scope | (see below) | OAuth scopes requested during login |
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:
| Tier | Character Slots | Features |
|---|---|---|
| Builder | 10 | Full character creation, roster management, handoff |
| Master | 50 | Everything 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.actorhandle (or a custom domain handle) - Their own
actor.rpg.statsandactor.rpg.spriterecords - 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
- Add a wildcard CNAME record for your domain pointing to
rpg.actor:
This allows any*.yourgame.com CNAME rpg.actor.name.yourgame.comsubdomain to route to the rpg.actor server. - 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.
- Create characters using the domain. When creating or renaming a character, select your domain from the dropdown to assign handles like
hero.yourgame.com.
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
| Lexicon | Key | Purpose |
|---|---|---|
actor.rpg.stats | self | Character stats supporting multiple systems per record (D&D, DCC, RMMZ, and others) |
actor.rpg.sprite | self | Standardized sprite sheet in PNG + animation metadata |
actor.rpg.generator | self | Separated sprite layers and configuration for recomposition |
actor.rpg.master | TID | Master validation records referencing a player's stats |
Equipment Records
| Lexicon | Key | Repo | Purpose |
|---|---|---|---|
equipment.rpg.give | any | Provider's PDS | Attestation that a provider granted an item to a player |
equipment.rpg.item | any | Player's PDS | Player-held item record with asset blob and display metadata |
The raw schemas for each lexicon are available here:
- actor.rpg.stats.json — character stats
- actor.rpg.sprite.json — sprite sheets
- actor.rpg.generator.json — sprite generator layers
- actor.rpg.master.json — master validations
- equipment.rpg.give.json — equipment provider attestations
- equipment.rpg.item.json — player equipment items
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)
| Endpoint | Description |
|---|---|
| GET /api/actors | All indexed actor DIDs with collection metadata (hasSprite, hasStats, timestamps) |
| GET /api/actors/full | Full 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/stats | Registry summary metrics (actor count, system count, master authority count) |
| GET /api/health | Service health, actor/master counts, and uptime |
Masters (Public)
| Endpoint | Description |
|---|---|
| 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)
| Endpoint | Description |
|---|---|
| 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)
| Endpoint | Description |
|---|---|
| GET /api/equipment | Equipment 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)
| Endpoint | Description |
|---|---|
| GET /api/creator/pricing | Current pricing tiers, early bird availability, and PDS status |
| GET /api/creator/check?did=... | Check if a DID is a registered creator account |
Account (Authenticated)
| Endpoint | Description |
|---|---|
| POST /api/pds-login | Password login for rpg.actor native accounts |
| POST /api/account/update-email | Update PDS account email (requires DID + current password) |
| POST /api/account/change-password | Change account password (requires DID + current + new password) |
| POST /api/refresh-actor | Trigger immediate cache refresh after a stat or sprite mutation |
| POST /api/contact | Contact form submission |
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.