Data Model (C#)¶
Note
This page covers what shape Echo’s data takes: the Sighting record (the
core payload uploaded per observed character) and every other type exchanged with
echovault.gg. For how this data travels, see Transfer Protocol (HTTP).
Every type below is declared in Echo’s plugin and (presumably) its server.
Registration and session¶
public record RegisterRequest(
int ProtocolVersion,
string PluginVersion);
public record RegisterResponse(
string UploaderId,
string ApiKey,
string HmacSecret);
public record SessionRequest(
int ProtocolVersion,
string UploaderId,
string ApiKey);
public record SessionResponse(
string Token,
DateTimeOffset ExpiresAt,
string Tier = "unverified");
See POST /v1/auth/register and POST /v1/auth/session in Transfer Protocol (HTTP).
Ingest: the sighting payload¶
The three types that make up the /v1/ingest request body — EquipSlot,
ReporterSelf, and Sighting itself — plus the envelope, IngestBatch, that
wraps them.
public record ReporterSelf(
ushort TerritoryId,
float X,
float Y,
float Z);
public record EquipSlot(
uint Id,
byte Variant,
byte Stain0,
byte Stain1);
public record Sighting(
ulong ContentId,
string Name,
uint HomeWorldId,
uint CurrentWorldId,
ushort TerritoryId,
float X,
float Y,
float Z,
byte JobId,
byte Level,
string? FcTag,
string? CustomizeBase64,
DateTimeOffset SeenAtUtc,
string Source = "sweep",
ulong AccountId = 0uL,
ushort TitleId = 0,
byte GrandCompany = 0,
List<EquipSlot>? Equipment = null,
ulong MainhandModel = 0uL,
ulong OffhandModel = 0uL,
string? HomeWorldName = null,
ReporterSelf? Reporter = null,
ushort MountId = 0,
ushort OnlineStatusId = 0,
ushort DutyId = 0,
bool? IsOnline = null);
public record IngestBatch(
int ProtocolVersion,
string PluginVersion,
ReporterSelf Reporter,
List<Sighting> Sightings,
bool AutoSearchEnabled = false);
public record IngestResponse(
int Accepted);
Sighting schema¶
This is the core payload type, and populated by each of the six collector code paths described below.
Field |
Type |
Notes |
|---|---|---|
|
ulong (string) |
Permanent per-character identifier. Survives character renames. |
|
string |
Full character name at time of capture. |
|
uint / string |
Character’s home world. |
|
uint |
World the character is currently visiting/playing on, if different. |
|
ushort |
Current zone/instance ID. |
|
float |
Exact in-game position at time of capture (sweep/spawn sources only). |
|
byte |
Current class/job and level. |
|
string, optional |
Free Company tag, if any. |
|
string, optional |
Base64 of the game’s raw “Customize” byte block — race, clan, gender, face, hair, skin/eye/hair color, and every other appearance slider, sufficient to reconstruct the character’s exact appearance. |
|
timestamp |
When this sighting occurred. |
|
string |
One of |
|
ulong (string) |
Square Enix service-account-level identifier, shared across every character on the same account. See Policy Violations for why this specific field is independently flagged by both Square Enix and the Dalamud project as a safety issue when harvested from other players. |
|
ushort |
Currently equipped title. |
|
byte |
Grand Company + rank byte ( |
|
list of |
One entry per gear slot: |
|
ulong |
Weapon model IDs for both weapon slots. |
|
|
The uploading player’s own zone + exact coordinates at ingest time (see
|
|
ushort |
Currently summoned mount, if any. |
|
ushort |
Online/away/busy/roleplaying/etc. status. |
|
ushort |
Current duty/instance content ID (namecache/Party-Finder source only). |
|
bool, optional |
|
Collection channels¶
Echo runs four independent collectors:
Collector |
Cadence |
Mechanism |
|---|---|---|
|
5s full sweep, 1s spawn-diff |
Iterates every |
|
Party: 15s · Rosters: 60s · Search: 15s |
Reads the local/cross-realm party ( |
|
Event-driven |
Captures every Party Finder listing received ( |
|
Randomized, opt-in |
Automates the game’s own Player Search feature — see
The auto-search sweep (DMA) in Transfer Protocol (HTTP). Source tag: |
Server config¶
public record ConfigResponse(
string MinPluginVersion,
int CaptureCadenceSeconds,
bool IngestEnabled,
int MinEmitIntervalSeconds = 10,
int SocialCadenceSeconds = 900);
See GET /v1/config in Transfer Protocol (HTTP).
Coverage / scan targets¶
public record ScanTargetsRequest(
int ProtocolVersion,
uint WorldId,
int TargetsRevision = 0);
public record ScanTarget(
int TerritoryId,
int NewPlayers7d,
int? LastSweptHoursAgo,
int Sightings7d);
public record WorldCompleteness(
int WorldId,
int Indexed,
int? EstimatedPopulation,
double? CompletenessPct,
double? NoveltyPct);
public record ScanTargetsResponse(
string GeneratedAt,
int WorldId,
int TargetsRevision,
List<ScanTarget> Targets,
List<WorldCompleteness> DcWorlds);
See POST /v1/scan-targets in Transfer Protocol (HTTP).
Scanner stats¶
public record ScannerStatsRequest(
int ProtocolVersion);
public record ScannerBestWeek(
string WeekStart,
int Count);
public record ScannerStatsResponse(
long LifetimeSightings,
int WeekSightings,
string WeekStart,
ScannerBestWeek? BestWeek,
int CharactersObserved,
int CharactersContributed,
int TerritoriesCovered,
int? PercentileBand,
string ComputedAt);
Note
The client waits out a full 15 minutes regardless of whether the previous attempt succeeded. If the very first fetch fails, the Progress tab keeps showing “No stats yet - they appear once uploads flow.” until the next attempt, 15 minutes later.
See POST /v1/scanner/stats and Motives of Echo users for what this data is used for.
Claims and verification¶
public record LinkStartRequest(
int ProtocolVersion,
ulong ContentId,
string CharacterName,
uint HomeWorldId);
public record LinkStartResponse(
string Code,
DateTimeOffset ExpiresAt);
public record LinkStartError(
string Error,
int? AgeDays = null,
int? AgeRequiredDays = null,
int? Observed = null,
int? ObservedRequired = null);
public record VerifyStartRequest(
int ProtocolVersion,
string LodestoneId,
string CharacterName,
string HomeWorldName,
ulong ContentId);
public record VerifyStartResponse(
string Code);
public record VerifyCompleteRequest(
int ProtocolVersion);
public record VerifyCompleteResponse(
bool Verified,
string? Reason);
LinkStartError is what /v1/claims/link/start returns on failure instead of
LinkStartResponse; LinkClaimMessages.Describe pattern-matches on its
Error string ("standing", "banned", "protocol_too_old", "not_registered",
"bad_content_id", "bad_character_name", "unreachable", or an unrecognized code)
to produce the in-game chat message. See POST /v1/claims/link/start and
the two /v1/auth/verify/* endpoints in Transfer Protocol (HTTP).
Appeals and generic errors¶
public record AppealRequest(
string? Note);
public record ErrorResponse(
string Error);
ErrorResponse is the generic { "error": "..." } shape EchoApiClient
attempts to parse from any non-success response (ReadErrorCodeAsync) to extract a
machine-readable error code — used by the session-refresh and link/verify flows to
distinguish, for example, "unknown_key" (triggers a fresh registration) from a
rate limit (response.StatusCode == 429 maps to the synthetic code
"rate_limited" even without a body). See POST /v1/appeals in
Transfer Protocol (HTTP).
Unused¶
Two more types exist but are never used. They are presumably shared with server-side or website-only code.
public record PlayerProfileResponse(
ulong ContentId,
string Name,
uint HomeWorldId,
uint CurrentWorldId,
ushort LastTerritoryId,
byte Level,
byte JobId,
string? FcTag,
DateTimeOffset FirstSeenAt,
DateTimeOffset LastSeenAt,
ushort TitleId = 0,
byte GrandCompany = 0,
string? EquipmentJson = null,
string? AvatarUrl = null,
string? PortraitUrl = null,
string? HomeWorldName = null);
public record ScanTargetsWorld(
string GeneratedAt,
int WorldId,
List<ScanTarget> Targets);
PlayerProfileResponse in particular is worth noting even though the plugin never
touches it: its shape — FirstSeenAt/LastSeenAt, an AvatarUrl and
PortraitUrl — strongly suggests it is exactly what backs each character’s public
profile page on echovault.gg.
Local storage¶
<pluginConfigDir>/instances/<ContentId as 16-hex-digit>/
instance.lock - exclusive file lock; prevents two game clients logged into
the same character from double-reporting
keys.bin - DPAPI-encrypted (Windows, per-user) blob containing:
uploaderId, apiKey, hmacSecret, sessionToken,
sessionExpiresAt, tier
outbox.jsonl - queue of not-yet-uploaded Sighting JSON lines, capped at
50 MB (oldest entries evicted first when full), drained in
batches of up to 200 lines roughly every 10 seconds
keys.bin under the Dalamud plugin config directory (InstancePaths) is protected
with System.Security.Cryptography.ProtectedData (DPAPI)
using a fixed static entropy value ("echo-keystore-v1") — this protects the file
from being read by a different Windows user account on the same machine, but does not
add any protection beyond what DPAPI itself provides, and is irrelevant to what is
sent over the network, which is plain JSON over TLS. The keys.bin payload itself
is the StoredCredentials type — see the “Local-only types” section below.
Local-only types¶
These two types never appear on the wire at all — they exist purely to persist local state to disk between sessions.
public record StoredCredentials(
string UploaderId,
string ApiKey,
string HmacSecretBase64,
string? SessionToken,
DateTimeOffset? SessionExpiresAt,
string? Tier = null);
public sealed record PersistedSettings(
bool CaptureEnabled = true,
bool SocialCaptureEnabled = true,
bool NameCacheCaptureEnabled = true,
bool SearchCaptureEnabled = true,
bool ContextMenuLinkEnabled = true,
bool AutoSearchEnabled = false,
bool OverlayEnabled = false,
bool OverlayClickThrough = false,
bool OverlayLocked = false);