Derakkuma ATProto model#
Derakkuma publishes maimai DX data as ATProto records. User-owned records are compact and link to a canonical song/chart catalog instead of embedding repeated chart metadata.
Authorities#
Canonical catalog records are published by did:plc:4uoc2as443j2fg2f6xfsogqs:
com.derakkuma.songcom.derakkuma.chart
The catalog source is https://dp4p6x0xfi5o9.cloudfront.net/maimai/data.json.
Users publish personal records to their own PDS:
com.derakkuma.profilecom.derakkuma.playcom.derakkuma.bestcom.derakkuma.friendcom.derakkuma.favoriteSongcom.derakkuma.circlecom.derakkuma.circleMember
com.derakkuma.profile#
The user's current maimai DX profile. This record is updated in place.
Key strategy: literal rkey self. In the lexicon this must be declared as "key": "literal:self"; bare "literal" is invalid and can make lexicon validators like pdsls reject com.derakkuma.profile even though the other any/tid collections validate.
{
"$type": "com.derakkuma.profile",
"playerName": "でらっくま",
"title": "ちほーの旅人",
"titleRarity": "gold",
"rating": 12345,
"stars": 123,
"comment": "よろしくお願いします",
"profileImage": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"trophyPlateImage": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"ratingPlateImage": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"partnerImage": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"courseImage": null,
"classImage": null,
"friendCode": "123456789012345",
"createdAt": "2026-05-22T00:00:00Z",
"updatedAt": "2026-05-22T00:00:00Z"
}
profileImage, partnerImage, courseImage, and classImage are uploaded from the SEGA CDN. trophyPlateImage and ratingPlateImage are bundled Derakkuma maimai UI assets uploaded from composeResources/files/maimai/, derived from titleRarity and the parsed rating plate name. Consumers should render the blobs when available and can use titleRarity as a semantic fallback.
friendCode is optional and only published after the user explicitly confirms the separate friend-code publish setting. Consumers must treat it as absent by default.
Catalog graph#
com.derakkuma.song --charts[] URI array--> com.derakkuma.chart
com.derakkuma.chart --song strongRef-------> com.derakkuma.song
com.derakkuma.play --chart strongRef------> com.derakkuma.chart
com.derakkuma.best --chart strongRef------> com.derakkuma.chart
com.derakkuma.favoriteSong --song strongRef-> com.derakkuma.song
com.derakkuma.circleMember --circle AT URI-> com.derakkuma.circle
com.derakkuma.circleMember --profile AT URI-> com.derakkuma.profile (only when resolved, e.g. self)
To avoid circular CID churn, chart.song is the canonical strong reference, while song.charts is a convenience array of chart AT URIs. Consumers should trust chart.song if the two directions ever disagree.
Records#
com.derakkuma.song#
Canonical song metadata from the maimai data API. Published by the catalog authority only.
Key strategy: song<sha256(songId)[0:16]>.
{
"$type": "com.derakkuma.song",
"songId": "君の知らない物語",
"title": "君の知らない物語",
"artist": "supercell「化物語」",
"category": "POPS&アニメ",
"bpm": 181,
"imageName": "b9d06643b0f9db1f154aad251470631bb688d1b5fb36566738d2c1f3cd488566.png",
"coverArt": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"version": "GreeN",
"releaseDate": "2013-07-11",
"isNew": false,
"isLocked": false,
"comment": null,
"charts": ["at://did:plc:4uoc2as443j2fg2f6xfsogqs/com.derakkuma.chart/chart..."],
"updatedAt": "2026-05-22T00:00:00Z"
}
coverArt is uploaded by the catalog publisher from https://dp4p6x0xfi5o9.cloudfront.net/maimai/img/cover/{imageName}. imageName is kept as a stable upstream/source identifier, but consumers should prefer the blob when rendering ATProto data.
com.derakkuma.chart#
Canonical chart/sheet metadata linked to a song. Published by the catalog authority only.
Key strategy: chart<sha256(songId | version | type | difficulty)[0:16]>.
{
"$type": "com.derakkuma.chart",
"song": {
"uri": "at://did:plc:4uoc2as443j2fg2f6xfsogqs/com.derakkuma.song/song...",
"cid": "..."
},
"songId": "君の知らない物語",
"type": "dx",
"difficulty": "basic",
"level": "3",
"levelValue": "3",
"internalLevel": null,
"internalLevelValue": "3",
"noteDesigner": "-",
"noteCounts": { "tap": 53, "hold": 18, "slide": 4, "touch": 6, "break": 3, "total": 84 },
"regions": { "jp": false, "intl": false, "usa": false, "cn": false },
"version": "maimaiでらっくす PLUS",
"updatedAt": "2026-05-22T00:00:00Z"
}
For 宴/utage charts, Derakkuma stores version as "utage", while difficulty is the API's utage subtype label rather than a normal BASIC/ADVANCED/etc. difficulty:
{
"$type": "com.derakkuma.chart",
"songId": "(宴) Love You",
"type": "utage",
"difficulty": "【協】",
"level": "*",
"version": "utage"
}
com.derakkuma.play#
A single play event published by a user. play records do not embed song/chart metadata; they strong-reference com.derakkuma.chart.
{
"$type": "com.derakkuma.play",
"chart": {
"uri": "at://did:plc:4uoc2as443j2fg2f6xfsogqs/com.derakkuma.chart/chart...",
"cid": "..."
},
"dedupeKey": "2026/03/14 15:12|at://did:plc:4uoc2as443j2fg2f6xfsogqs/com.derakkuma.chart/chart...",
"achievement": "92.4198",
"playedAt": "2026/03/14 15:12",
"scoreRank": "aa",
"fcStatus": "",
"syncStatus": "sync",
"dxScore": { "achieved": 0, "total": 0 },
"dxStar": 3,
"isNewRecord": false,
"trackNum": 1,
"matchingRank": 1,
"fast": 0,
"late": 0,
"maxCombo": { "achieved": 0, "total": 0 },
"maxSync": { "achieved": 0, "total": 0 },
"notes": {},
"rating": 12345,
"ratingDelta": 0,
"createdAt": "2026-05-22T00:00:00Z"
}
dedupeKey is included as a convenience for client/AppView dedupe. It is not the chart identity.
dxStar is optional and, when present, is an integer from 1 to 5 representing the DX star badge shown by DX NET for the play's deluxe score. It is omitted when no DX star badge was parsed.
com.derakkuma.best#
A user's personal best for one canonical chart.
Key strategy: best<hash(chartUri)>.
{
"$type": "com.derakkuma.best",
"chart": {
"uri": "at://did:plc:4uoc2as443j2fg2f6xfsogqs/com.derakkuma.chart/chart...",
"cid": "..."
},
"achievement": "100.5000",
"scoreRank": "sssp",
"fcStatus": "ap",
"syncStatus": "fsdp",
"dxScore": { "achieved": 3050, "total": 3050 },
"dxStar": 5,
"playCount": 12,
"lastPlayed": "2026-05-22T00:00:00Z",
"createdAt": "2026-05-22T00:00:00Z",
"updatedAt": "2026-05-22T00:00:00Z"
}
dxStar has the same semantics as on com.derakkuma.play: optional integer 1 through 5, omitted when unavailable.
com.derakkuma.favoriteSong#
A user's favorite-song list entry. Favorite records do not embed song metadata or jacket blobs; they strong-reference com.derakkuma.song.
{
"$type": "com.derakkuma.favoriteSong",
"song": {
"uri": "at://did:plc:4uoc2as443j2fg2f6xfsogqs/com.derakkuma.song/song...",
"cid": "..."
},
"orderId": 12,
"observedAt": "2026-05-22T00:00:00Z",
"createdAt": "2026-05-22T00:00:00Z",
"updatedAt": "2026-05-22T00:00:00Z"
}
com.derakkuma.circle#
A snapshot of the user's current maimai DX circle. Circle ranking fields such as rank and total points live directly on this record; there is no separate com.derakkuma.circleRanking record.
Key strategy: deterministic opaque circle<hash(circleCode/name)>. The circle code/name may be used locally as stable rkey input, but the hash input is not published as circleKey.
{
"$type": "com.derakkuma.circle",
"circleCode": "A1B2C3D4",
"name": "でらっくま部",
"ownerName": "でらっくま",
"comment": "よろしくお願いします",
"tags": ["enjoy", "rating"],
"characterImage": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"backgroundImage": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"month": "2026-05",
"totalPoints": 123456,
"daysUntilReset": 12,
"rank": 42,
"nextRewardPoints": 5000,
"festaMessage": "FESTA開催中",
"observedAt": "2026-05-22T00:00:00Z",
"createdAt": "2026-05-22T00:00:00Z",
"updatedAt": "2026-05-22T00:00:00Z"
}
circleCode is optional and only published after the user explicitly confirms the separate circle-code publish setting, mirroring the profile friend-code opt-in. Consumers must treat it as absent by default.
There is intentionally no circleKey field. circleKey used to duplicate circleCode and should not be published.
com.derakkuma.circleMember#
A snapshot of a member in a maimai DX circle. Member records link back to the parent circle record via circle; they do not copy the circle's code or any synthetic circle key.
Key strategy: self for the publisher's own member record, deterministic opaque cm<hash(circleRkey/member identity)> for other visible members.
{
"$type": "com.derakkuma.circleMember",
"circle": "at://did:plc:userdid/com.derakkuma.circle/circle...",
"subject": "did:plc:userdid",
"profile": "at://did:plc:userdid/com.derakkuma.profile/self",
"displayName": "でらっくま",
"title": "ちほーの旅人",
"rating": 12345,
"points": 1234,
"status": "owner",
"icon": {
"$type": "blob",
"ref": { "$link": "..." },
"mimeType": "image/png",
"size": 12345
},
"observedAt": "2026-05-22T00:00:00Z",
"createdAt": "2026-05-22T00:00:00Z",
"updatedAt": "2026-05-22T00:00:00Z"
}
circle is required and is the AT URI of the com.derakkuma.circle record this member belongs to. profile is optional and should only be present when the member is resolved to a Derakkuma ATProto profile, such as the publisher's own self member. subject is the resolved DID when known.
circleMember must never publish circleCode or circleKey. Circle code visibility is controlled only by the parent com.derakkuma.circle.circleCode opt-in.
Chart resolution in the app#
Before publishing play or best, the app resolves DX NET scraped data to a catalog chart by:
- normalized song title / songId
- chart type (
dx,std, orutage) - difficulty (
basic,advanced,expert,master,remaster; for 宴, the subtype label such as【協】/【宴】) - level as a sanity check when available
If a chart cannot be resolved, the app skips publishing that record and records an ATProto sync warning. This keeps the graph clean during hard-cut migration.
DX NET exposes 宴/utage in music sort using filter value 10. Historical backfill tracks idxs fetched from that filter and marks those records with generic difficulty utage; catalog resolution then resolves by title/type/level against type: "utage" charts, ignoring the catalog's subtype label when the app only knows generic utage.
HappyView queries#
HappyView joins user records through the catalog:
play.chart.uri -> com.derakkuma.chart
chart.song.uri -> com.derakkuma.song
best.chart.uri -> com.derakkuma.chart
chart.song.uri -> com.derakkuma.song
circleMember.circle -> com.derakkuma.circle
com.derakkuma.getFriendsPlaysreturns recent plays from the user's Derakkuma friend graph, enriched with profile/chart/song metadata.com.derakkuma.getRecordFeedreturns a mixed feed of followed users plus public Atmosphere plays, enriched with profile/chart/song metadata.com.derakkuma.getLeaderboardacceptschart/chartUri(preferred) orsongName+difficulty(compatibility), then returns enriched best records sorted by achievement.
Publishing order#
- Publish lexicons from
lexicons/schema-records/. - Publish catalog records with
scripts/publish_catalog.pyusing the catalog authority account. - Deploy/update HappyView query lexicons/scripts.
- Delete early-development legacy user
com.derakkuma.playandcom.derakkuma.bestrecords. - Republish user data from the app.
Operational commands#
Publish lexicons:
deploy/happyview/scripts/publish-lexicons.sh
publish-lexicons.sh publishes lexicons/schema-records/com.derakkuma.*.json with goat lex publish using the current goat login session.
Publish the catalog:
ATP_APP_PASSWORD=... \
CATALOG_REPO_DID=did:plc:4uoc2as443j2fg2f6xfsogqs \
./scripts/publish_catalog.py
scripts/publish_catalog.py creates its own ATProto session. By default it uses CATALOG_REPO_DID as the auth identifier and resolves that DID's PDS. Override with ATP_IDENTIFIER and/or ATP_AUTH_PDS_URL if the catalog authority needs a handle or explicit PDS URL. ATP_AUTH_TOKEN is still accepted as an escape hatch when you already have an access JWT.
Publishing is parallelized with separate knobs: CATALOG_SONG_CONCURRENCY=1 by default for blob-upload-heavy song records, and CATALOG_CHART_CONCURRENCY=8 by default for chart records. CATALOG_CONCURRENCY can be used as a fallback for both.
On restart, the publisher lists existing song records, checks deterministic song rkeys, and skips already-published song records that already have coverArt, reusing their uri/cid for chart strong refs.
It also lists existing chart records and skips charts whose deterministic rkey exists and whose song strongRef matches the current catalog song uri/cid; stale chart refs are republished.
All HTTP calls use CATALOG_REQUEST_TIMEOUT=30 seconds by default so one dead connection cannot stall the publish forever.
Install/update HappyView query scripts on the AppView host:
python3 deploy/happyview/scripts/install_happyview_social_lexicons.py