XProtocol Graph Store — Full Specification
Protocol Version: 0.2 (Draft)
Date: 2026-05-31
License: CC BY 4.0 (this document) · Apache 2.0 (code)
Status: Proposed — not yet ratified as a community standard
Namespace: xp.graph.*
Depends on: XProtocol-Specification.md (core protocol), XProtocol Event Store (xp.store.*)
Repository: gitlab.com/xprotocol/xprotocol-graph-store (proposed)
Abstract
The XProtocol Graph Store extends the XProtocol Event Store with a structured annotation system that transforms the flat event collection into a navigable, queryable graph. Every stored event can carry a mutable, authorized, auditable dictionary of typed metadata — linking it to other events, grouping it with other entities, tagging it for workflows, tracing it across systems, and organizing it for recipients — without ever touching the immutable signed payload.
The result is a data substrate that simultaneously serves as a document store, a graph database, a distributed tracing system, an observability platform, a workflow engine, and an AI agent memory layer — all under a single cryptographic identity model, with provable authorship and access control on every record and every mutation to every record.
1. Design Principles
1.1 — Immutability of Events, Mutability of Annotations
XProtocol events are cryptographically immutable. The signed payload, the sender key, the recipient key, the timestamp, and the content hash are permanent and unforgeable. Changing any of them invalidates the signature.
The Graph Store adds a second layer — the annotation envelope — that is explicitly mutable, explicitly unencrypted, and explicitly authorized. Annotations sit outside the signature boundary. The event proves what happened; the annotations describe how it relates to everything else.
┌─────────────────────────────────────────────────────┐
│ Stored Graph Node │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ XpEvent (immutable, signed) │ │
│ │ - id (SHA-256 content hash) │ │
│ │ - sender, recipient, kind, timestamp │ │
│ │ - payload (encrypted ciphertext) │ │
│ │ - signature (Ed25519, 64 bytes) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Annotation Envelope (mutable, authorized) │ │
│ │ - thread.* (conversation structure) │ │
│ │ - recipient.* (personal inbox state) │ │
│ │ - trace.* (distributed tracing) │ │
│ │ - groups (membership) │ │
│ │ - links (typed relationships) │ │
│ │ - workflow.* (process state) │ │
│ │ - versioning.* (supersession chain) │ │
│ │ - ai.* (semantic tags) │ │
│ │ - custom.* (arbitrary extensions) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
1.2 — Every Mutation Is a Signed Event
Annotations are never silently mutated in place. Every annotation change
is effected by sending an xp.graph.annotate event to the store — a
signed event that itself becomes a permanent record. The annotation
history for any stored event is therefore a first-class queryable dataset:
who annotated this event, what they changed, and when.
This means the Graph Store has no back-door writes. Every state change is traceable, auditable, and attributable to a specific key.
1.3 — Authorization Is Per-Field, Not Per-Record
Every annotation key declares its own authorization level. Some fields can only be set by the sender. Some only by the recipient. Some by either participant. Some by explicitly granted third-party keys. Some only by the store itself. These authorizations are declared in the Annotation Schema for each event kind and enforced by the store on every write.
1.4 — The Graph Is a Query, Not a Structure
Relationships between events (links, threads, groups, traces) are
expressed as annotations on event nodes, not as separate edge entities.
A "thread" is not a database table — it is a query:
filter: { annotations.thread.thread_id == 'X' }.
A "group" is not a collection object — it is a query:
filter: { annotations.groups contains 'Y' }.
The graph structure emerges from annotations and is traversed via queries.
1.5 — All Annotations Are Queryable Without Decryption
Annotations are stored in plaintext in the annotation envelope. They are never encrypted (by definition — they exist to be queryable by the store without payload decryption). Sensitive information must not be placed in annotations. Annotations describe relationships and structure; content belongs in the encrypted payload.
1.6 — Array Conflict Resolution
Array annotation fields (groups, links, threads, external_refs,
recipient.labels, ai.entities, ai.topics) require defined conflict
semantics when two annotators write concurrently.
Conflict resolution rule: last-writer-wins with a deterministic
tiebreaker. The ordering key for any array annotation write is the
tuple (annotated_at_ms, annotating_key_fingerprint), compared
lexicographically. When two xp.graph.annotate events arrive with
identical millisecond timestamps, the one whose sender key fingerprint
sorts later lexicographically wins. Key fingerprints are deterministic
fixed values, making this tiebreaker always unambiguous.
Duplicate entry semantics: Set-membership array fields enforce
uniqueness by a canonical key. An append introducing a duplicate is
silently deduplicated — the higher-ordered entry (by conflict key) is
retained. A remove_from targeting a non-existent entry is a no-op,
not an error.
| Field | Uniqueness Key |
|---|---|
groups |
group_id |
links |
(event_id, rel) pair |
threads |
thread_id |
external_refs |
(system, entity, id) triple |
recipient.labels |
label string value |
ai.entities |
entity string value |
ai.topics |
topic string value |
Ordering guarantees: Array fields do NOT guarantee insertion order
in query results unless an explicit sort is applied. Consumers must
not rely on array element position for semantic meaning. For threads,
the position field within each thread entry is the authoritative
ordering mechanism — not array index position.
2. Annotation Namespaces
The annotation envelope is a structured dictionary organized into namespaces. Each namespace covers a distinct concern.
2.1 — Standard Namespaces
| Namespace | Purpose | Section |
|---|---|---|
thread.* |
Conversation threading and reply structure | §3 |
recipient.* |
Per-recipient inbox state (read, folder, star) | §4 |
trace.* |
Distributed tracing across systems | §5 |
groups |
Group membership (users, events, data) | §6 |
links |
Typed relationships to other events | §7 |
workflow.* |
Process and approval workflow state | §8 |
versioning.* |
Version chains and supersession | §9 |
ai.* |
AI-derived semantic tags and summaries | §10 |
custom.* |
Application-specific extensions | §11 |
2.2 — Reserved vs Extension Namespaces
thread, recipient, trace, groups, links, workflow,
versioning, and ai are reserved standard namespaces defined by this
specification. Their schemas are protocol-standard.
custom is the extension namespace. Applications declare their own
annotation schemas under custom.* without conflicting with the standard
namespaces.
Vendors may also declare named top-level namespaces under their own
domain: salesforce.annotations.*, stripe.annotations.*. These follow
the same DNS-ownership rules as event kind namespaces.
3. Thread Namespace — Conversation Structure
3.1 — Purpose
Links events into threaded conversations without a separate thread entity. Any sequence of events can be organized into a thread after the fact. Events can belong to multiple threads simultaneously.
3.2 — Canonical Representation
The thread namespace always uses the plural array form threads,
even when an event belongs to only one thread. The singular thread.*
dot-notation is used when referring to field names within an entry, but
the top-level annotation key is always threads (an array). Consumers
must never assume the singular form. This eliminates the ambiguity of
handling both a scalar and an array representation.
"annotations": {
"threads": [
{
"thread_id": "thread-support-case-123",
"root_event_id": "sha256-first-message",
"reply_to_event_id": null,
"depth": 0,
"position": 1,
"subject": "Billing issue Q2",
"closed": false
},
{
"thread_id": "thread-internal-review",
"root_event_id": "sha256-review-start",
"reply_to_event_id": "sha256-manager-comment",
"depth": 2,
"position": 7,
"subject": null,
"closed": false
}
]
}
A single-thread event uses the same structure with a one-element array. No special-casing. No singular form ever.
3.3 — Fields (per thread entry)
| Field | Type | Auth | Mutable | Indexed | Description |
|---|---|---|---|---|---|
thread_id |
string (UUID) | PARTICIPANTS | No | Yes | Identifies the thread this entry belongs to |
root_event_id |
string (SHA-256) | PARTICIPANTS | No | Yes | The first event in this thread |
reply_to_event_id |
string (SHA-256) | SENDER_ONLY | No | Yes | Direct parent event; null for root events |
depth |
integer | PARTICIPANTS | No | Yes | Nesting depth (0 = root, 1 = direct reply, etc.) |
position |
integer | PARTICIPANTS | No | Yes | Sequential position within the thread; authoritative sort key |
subject |
string | PARTICIPANTS | Yes | Yes | Human-readable thread title; null if untitled |
closed |
boolean | PARTICIPANTS | Yes | Yes | Whether new replies are permitted |
3.4 — Query Patterns
// Retrieve all events in a thread, ordered by position
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.threads[*].thread_id", "op": "eq",
"value": "thread-abc123" },
"sort": [{ "field": "annotations.threads[thread_id=thread-abc123].position",
"direction": "asc" }]
}
}
// Retrieve direct replies to a specific event
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.threads[*].reply_to_event_id", "op": "eq",
"value": "sha256-parent-event" }
}
}
// Retrieve all root-level events (depth 0) in a thread
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.threads[*].thread_id", "op": "eq", "value": "thread-abc123" },
{ "field": "annotations.threads[*].depth", "op": "eq", "value": 0 }
]
}
}
}
3.5 — Conflict Semantics for Threads
threads is an array field with uniqueness key thread_id. Two
participants may independently add the same event to the same thread
(e.g., both annotating thread_id: "thread-abc123"). The store
deduplicates by thread_id, retaining the entry from the
higher-ordered (annotated_at_ms, annotating_key_fingerprint) pair.
position values are assigned by the annotating participant and are
not globally coordinated — in the rare case of a position collision,
query results are sorted stably by (position, annotated_at_ms).
4. Recipient Namespace — Personal Inbox State
4.1 — Purpose
Organizes events from the recipient's personal perspective — read state, folders, stars, snooze — independently of how the sender organized them. All recipient annotations are RECIPIENT_ONLY. Different recipients of the same event have completely independent recipient views.
4.2 — Fields
| Field | Type | Auth | Mutable | Indexed | Description |
|---|---|---|---|---|---|
recipient.read |
boolean | RECIPIENT_ONLY | Yes | Yes | Whether the recipient has read this event |
recipient.read_at |
integer (unix ms) | RECIPIENT_ONLY | Yes | No | When it was first read |
recipient.starred |
boolean | RECIPIENT_ONLY | Yes | Yes | Starred/flagged for follow-up |
recipient.archived |
boolean | RECIPIENT_ONLY | Yes | Yes | Moved to archive |
recipient.folder |
string | RECIPIENT_ONLY | Yes | Yes | User-defined folder name |
recipient.labels |
array |
RECIPIENT_ONLY | Yes | Yes | Personal labels applied by recipient |
recipient.snoozed_until |
integer (unix ms) | RECIPIENT_ONLY | Yes | Yes | Snooze expiry; null if not snoozed |
recipient.priority |
enum | RECIPIENT_ONLY | Yes | Yes | urgent, high, normal, low |
recipient.note |
string | RECIPIENT_ONLY | Yes | No | Private note on this event (not shared) |
4.3 — Query Patterns
// All unread events addressed to me
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "recipient", "op": "eq", "value": "<my_public_key>" },
{ "field": "annotations.recipient.read", "op": "eq", "value": false }
]
},
"sort": [{ "field": "timestamp", "direction": "desc" }]
}
}
// All starred events in the "client-acme" folder
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.recipient.starred", "op": "eq", "value": true },
{ "field": "annotations.recipient.folder", "op": "eq", "value": "client-acme" }
]
}
}
}
5. Trace Namespace — Distributed Observability
5.1 — Purpose
Connects events across multiple services and systems into a complete, cryptographically-verifiable trace of a logical operation. Eliminates the need for separate tracing infrastructure (Jaeger, Zipkin, OpenTelemetry collectors) for XProtocol-native systems.
Because every annotation is a signed event, the trace record cannot be retroactively tampered with. Each system's contribution to a trace is signed by that system's key.
5.2 — ID Taxonomy
The trace namespace uses multiple distinct identifiers, each answering a different question about causality and context:
| ID Field | Type | Scope | Question It Answers |
|---|---|---|---|
trace.trace_id |
string (UUID) | All systems in one logical operation | What is the full end-to-end story? |
trace.span_id |
string (short UUID) | This event's contribution | What did this system do? |
trace.parent_span_id |
string (short UUID) | The span that caused this one | What triggered this system to act? |
trace.causation_id |
string (SHA-256) | The specific event that caused this | Which exact event (by content hash) triggered this? |
trace.correlation_id |
string (UUID) | User-visible operation reference | What is the human-readable reference number? |
trace.saga_id |
string (UUID) | Multi-step transaction | Which transaction is this event part of? |
trace.session_id |
string (UUID) | User authentication session | Which user session originated this? |
trace.batch_id |
string (UUID) | Batch operation | Which batch job is this event part of? |
trace.job_id |
string (UUID) | Scheduled job | Which cron or scheduled job triggered this? |
trace.request_id |
string (UUID) | Inbound gateway request | What was the originating API gateway request? |
5.3 — Fields
| Field | Type | Auth | Mutable | Indexed | Description |
|---|---|---|---|---|---|
trace.trace_id |
string | SENDER_ONLY | No | Yes | Top-level operation identifier; same across all systems |
trace.span_id |
string | SENDER_ONLY | No | Yes | This event's span within the trace |
trace.parent_span_id |
string | SENDER_ONLY | No | Yes | Parent span; null for root events |
trace.causation_id |
string (SHA-256) | SENDER_ONLY | No | Yes | SHA-256 ID of the event that caused this one |
trace.correlation_id |
string | SENDER_ONLY | No | Yes | User-visible request reference |
trace.saga_id |
string | SENDER_ONLY | No | Yes | Transaction/saga identifier |
trace.session_id |
string | SENDER_ONLY | No | Yes | Originating user session |
trace.batch_id |
string | SENDER_ONLY | No | Yes | Batch operation identifier |
trace.job_id |
string | SENDER_ONLY | No | Yes | Scheduled job identifier |
trace.request_id |
string | SENDER_ONLY | No | Yes | Inbound API gateway request ID |
All trace fields are SENDER_ONLY and immutable — they are set once by the sending system and cannot be altered. This prevents post-hoc modification of trace chains.
5.4 — Query Patterns
// Full trace reconstruction — every event in a logical operation
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.trace.trace_id", "op": "eq", "value": "trace-7f3a9c" },
"sort": [{ "field": "timestamp", "direction": "asc" }]
}
}
// All events caused by a specific event (direct children in trace tree)
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.trace.causation_id", "op": "eq",
"value": "sha256-parent-event-hash" }
}
}
// Everything in a failed saga for incident response
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.trace.saga_id", "op": "eq", "value": "saga-def456" },
"sort": [{ "field": "timestamp", "direction": "asc" }]
}
}
// All events from a user session across all services
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.trace.session_id", "op": "eq", "value": "sess-ghi789" }
}
}
5.5 — Trace Visualization: xp.graph.trace.view
The standard query for trace visualization. Returns a pre-computed DAG and waterfall dataset ready for rendering by any client — equivalent to Jaeger or Zipkin's trace view, built entirely from XProtocol events with no separate collector infrastructure.
{
"kind": "xp.graph.trace.view",
"payload": {
"trace_id": "trace-7f3a9c",
"layout": "waterfall"
}
}
layout values:
| Value | Description |
|---|---|
waterfall |
Gantt-style timeline; each span is a horizontal bar. Standard Jaeger/Zipkin view. |
dag |
Directed acyclic graph; nodes are events, edges are parent_span_id relationships. |
tree |
Hierarchical tree; root span at top, children indented by depth. |
Response:
{
"kind": "xp.graph.trace.view.result",
"payload": {
"trace_id": "trace-7f3a9c",
"layout": "waterfall",
"root_span_id": "span-1a0e",
"duration_ms": 342,
"span_count": 7,
"nodes": [
{
"span_id": "span-1a0e",
"parent_span_id": null,
"event_id": "sha256-abc123",
"event_kind": "xp.message.direct",
"sender_fingerprint": "a1b2:c3d4:...",
"started_at_ms": 1717000000000,
"duration_ms": 342,
"depth": 0,
"status": "ok",
"causation_id": null
},
{
"span_id": "span-2b1f",
"parent_span_id": "span-1a0e",
"event_id": "sha256-def456",
"event_kind": "salesforce.opportunity.create",
"sender_fingerprint": "e5f6:7890:...",
"started_at_ms": 1717000000045,
"duration_ms": 198,
"depth": 1,
"status": "ok",
"causation_id": "sha256-abc123"
}
],
"edges": [
{ "from": "span-1a0e", "to": "span-2b1f", "rel": "caused_by" }
],
"errors": [],
"truncated": false
}
}
status values per node: ok, error, timeout, cancelled.
The store determines status by checking whether the event's correlation
chain includes an xp.error response event.
duration_ms calculation: From this event's timestamp to the
latest timestamp of any event in its subtree (all descendants via
parent_span_id). For leaf spans (no children), duration is the
difference between this event's timestamp and any acknowledged response
event's timestamp (via correlation_id), or zero if no response is found.
errors array: A list of { span_id, event_kind, error_code } entries
for any span whose correlation chain contains an xp.error event. Allows
renderers to highlight the failure point in the waterfall.
The xp.graph.trace.view operation is declared optional in capability
announcements — basic Graph Stores may not implement it. Clients that need
visualization may fall back to composing it from xp.store.query results
filtered by trace_id.
6. Groups — Universal Membership
6.1 — Purpose
Groups provide a universal membership primitive that applies to any entity type simultaneously: events, users (keys), data records, services. A group is not a database table or a collection object — it is a label that makes its members discoverable via a common query.
6.2 — Group Definition
Groups are defined by sending an xp.graph.group.define event:
{
"kind": "xp.graph.group.define",
"payload": {
"group_id": "project-phoenix",
"group_type": "project",
"display_name": "Project Phoenix",
"description": "Q2 2026 platform rewrite initiative",
"owner_key": "<alice_public_key>",
"authorized_members": [
"<alice_public_key>",
"<bob_public_key>",
"<carol_public_key>"
],
"authorized_annotators": ["PARTICIPANTS"],
"visibility": "members_only"
}
}
The group definition event is itself stored in the Graph Store, queryable like any other event.
6.3 — Membership Annotation
Any event, user key metadata event, or data event can declare membership
by adding a groups annotation:
"annotations": {
"groups": [
{ "group_id": "project-phoenix", "group_type": "project", "added_at": 1717000000 },
{ "group_id": "client-acme", "group_type": "client", "added_at": 1717000000 },
{ "group_id": "q2-2026", "group_type": "period", "added_at": 1717000000 },
{ "group_id": "high-value", "group_type": "classification" }
]
}
6.4 — Group Types (Standard)
| Type | Description |
|---|---|
project |
A named initiative or project |
client |
A customer or client organization |
team |
An internal team or department |
period |
A time period (sprint, quarter, fiscal year) |
classification |
A data classification level |
workflow |
A workflow instance |
campaign |
A marketing or outreach campaign |
incident |
An incident response group |
custom |
Application-defined group type |
Custom group types are defined by the application under custom.* in
the group type field.
6.5 — Query Patterns
// Every event, user, and data record in project-phoenix
{
"kind": "xp.store.query",
"payload": {
"filter": {
"field": "annotations.groups[*].group_id",
"op": "contains",
"value": "project-phoenix"
}
}
}
// Count events per group for a dashboard
{
"kind": "xp.store.stats",
"payload": {
"group_by": "annotations.groups[*].group_id",
"filter": { "field": "timestamp", "op": "gte", "value": 1717000000 }
}
}
7. Links — Typed Relationships Between Events
7.1 — Purpose
Links express typed, directed relationships between events without requiring a separate edge entity. The graph of events is traversable via the link annotations.
7.2 — Link Structure
"annotations": {
"links": [
{
"event_id": "sha256-def456",
"rel": "caused_by",
"direction": "outbound",
"added_at": 1717000000,
"added_by": "<alice_public_key_fingerprint>"
},
{
"event_id": "sha256-ghi789",
"rel": "superseded_by",
"direction": "outbound"
},
{
"event_id": "sha256-jkl012",
"rel": "related_to",
"direction": "bidirectional"
}
]
}
7.3 — Standard Relationship Types
rel value |
Semantics | Direction |
|---|---|---|
caused_by |
This event was caused by the linked event | Outbound |
supersedes |
This event replaces the linked event | Outbound |
superseded_by |
This event has been replaced by the linked event | Outbound |
reply_to |
This event is a reply to the linked event | Outbound |
references |
This event references the linked event | Outbound |
part_of |
This event is a component of the linked event (saga, batch) | Outbound |
related_to |
General association; no causal implication | Bidirectional |
blocks |
This event represents a blocking dependency | Outbound |
blocked_by |
This event is blocked by the linked event | Outbound |
duplicates |
This event is a duplicate of the linked event | Bidirectional |
custom.* |
Application-defined relationship type | Declared by schema |
7.4 — Graph Traversal Query
// Fetch an event and all events it links to (one hop)
{
"kind": "xp.graph.traverse",
"payload": {
"start_event_id": "sha256-root",
"direction": "outbound",
"rel_filter": ["caused_by", "part_of"],
"max_depth": 3,
"include_annotations": true
}
}
The xp.graph.traverse query is the Graph Store extension to
xp.store.query — it follows link edges rather than filtering by field.
The result is a subgraph: a set of events connected by the traversed
links, returned with their full annotation envelopes.
7.5 — External System References
A special case of links: references to entities in external systems that do not use XProtocol. These carry no graph traversal semantics (there is no XProtocol event to traverse to) but provide a universal foreign key system across all integrated services.
"annotations": {
"external_refs": [
{ "system": "salesforce", "entity": "opportunity", "id": "opp-789" },
{ "system": "jira", "entity": "issue", "id": "PROJ-4421" },
{ "system": "stripe", "entity": "invoice", "id": "inv_abc123" },
{ "system": "github", "entity": "pull_request", "id": "1847" },
{ "system": "zendesk", "entity": "ticket", "id": "TKT-8823" }
]
}
Query by external reference:
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.external_refs[*].system", "op": "eq", "value": "jira" },
{ "field": "annotations.external_refs[*].id", "op": "eq", "value": "PROJ-4421" }
]
}
}
}
8. Workflow Namespace — Process State
8.1 — Purpose
Tracks the state of any event as it moves through a business process. Any event can be a workflow artifact. The workflow state is the annotation — not a separate workflow database entity. No workflow engine required. The workflow transitions are themselves signed annotation events, providing a complete, tamper-evident process history.
8.2 — Fields
| Field | Type | Auth | Mutable | Indexed | Description |
|---|---|---|---|---|---|
workflow.workflow_id |
string | PARTICIPANTS | No | Yes | Which workflow definition this follows |
workflow.instance_id |
string (UUID) | PARTICIPANTS | No | Yes | This specific workflow instance |
workflow.current_step |
string | AUTHORIZED_KEYS | Yes | Yes | Current step name |
workflow.status |
enum | AUTHORIZED_KEYS | Yes | Yes | active, completed, cancelled, failed |
workflow.assigned_to |
string (key) | AUTHORIZED_KEYS | Yes | Yes | Key currently responsible |
workflow.steps_completed |
array |
AUTHORIZED_KEYS | Yes | No | Ordered list of completed step names |
workflow.steps_remaining |
array |
AUTHORIZED_KEYS | Yes | No | Ordered list of remaining step names |
workflow.due_at |
integer (unix ms) | AUTHORIZED_KEYS | Yes | Yes | Deadline for current step |
workflow.started_at |
integer (unix ms) | STORE_MANAGED | No | Yes | When workflow was initiated |
workflow.completed_at |
integer (unix ms) | STORE_MANAGED | No | Yes | When workflow reached terminal state |
8.3 — Example
"annotations": {
"workflow": {
"workflow_id": "contract-approval-v3",
"instance_id": "wf-inst-7a3b2c",
"current_step": "legal-review",
"status": "active",
"assigned_to": "<legal_team_key_fingerprint>",
"steps_completed": ["submitted", "manager-approved"],
"steps_remaining": ["legal-review", "finance-sign-off", "countersign"],
"due_at": 1717172800
}
}
8.4 — Query Patterns
// All events currently in legal-review step, ordered by due date
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.workflow.current_step", "op": "eq", "value": "legal-review" },
{ "field": "annotations.workflow.status", "op": "eq", "value": "active" }
]
},
"sort": [{ "field": "annotations.workflow.due_at", "direction": "asc" }]
}
}
// Overdue workflow items (due_at in the past, still active)
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.workflow.status", "op": "eq", "value": "active" },
{ "field": "annotations.workflow.due_at", "op": "lt", "value": 1717000000 }
]
}
}
}
9. Versioning Namespace — Immutable Version Chains
9.1 — Purpose
Provides logical versioning for events that represent documents, records, or configurations that evolve over time. Because events are immutable, "updating" a record means publishing a new event and annotating it as a new version of the original. The version chain is navigable and the full history is always queryable.
9.2 — Fields
| Field | Type | Auth | Mutable | Indexed | Description |
|---|---|---|---|---|---|
versioning.version |
integer | SENDER_ONLY | No | Yes | Version number (1-based) |
versioning.entity_id |
string (UUID) | SENDER_ONLY | No | Yes | Stable identifier across all versions of this entity |
versioning.supersedes |
array |
SENDER_ONLY | No | Yes | SHA-256 IDs of prior versions this supersedes |
versioning.superseded_by |
string (SHA-256) | STORE_MANAGED | Yes | Yes | Set by store when a newer version is stored; null = current |
versioning.is_current |
boolean | STORE_MANAGED | Yes | Yes | True only for the latest non-deleted version |
versioning.status |
enum | AUTHORIZED_KEYS | Yes | Yes | active, deprecated, superseded, deleted |
versioning.changelog |
string | SENDER_ONLY | No | No | Human-readable description of changes from prior version |
versioning.major |
boolean | SENDER_ONLY | No | Yes | Whether this is a breaking change from the prior version |
9.3 — Status Lifecycle
versioning.status tracks the operational state of a versioned record
independently of the cryptographic supersession chain:
| Status | Meaning | is_current |
|---|---|---|
active |
Current, valid, in use | Yes (if latest version) |
deprecated |
Still valid but successor preferred; consumers should migrate | Yes (if latest version) |
superseded |
Replaced by a newer version; do not use | No |
deleted |
Soft-deleted; treat as non-existent for application purposes | No |
Soft delete semantics: The deleted status marks an event as
logically removed without physically deleting it from the store.
Physical deletion is not possible without breaking the event's signature
and the store's audit integrity. For GDPR right-to-erasure compliance,
deleted status combined with an xp.store.retention payload-revocation
event achieves the practical effect: the record is unfindable in normal
queries and the payload is cryptographically inaccessible, while the
event envelope (which contains no personal data) remains for audit
chain integrity.
Applications that query versioned entities should include a default
filter of versioning.status NOT IN ('superseded', 'deleted') unless
they explicitly need historical versions.
9.4 — Query Patterns
// Current active version of an entity
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.versioning.entity_id", "op": "eq", "value": "entity-uuid-abc" },
{ "field": "annotations.versioning.is_current", "op": "eq", "value": true },
{ "field": "annotations.versioning.status", "op": "eq", "value": "active" }
]
}
}
}
// Full version history of an entity (including deleted)
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.versioning.entity_id", "op": "eq",
"value": "entity-uuid-abc" },
"sort": [{ "field": "annotations.versioning.version", "direction": "asc" }]
}
}
// All deprecated records that need migration
{
"kind": "xp.store.query",
"payload": {
"filter": { "field": "annotations.versioning.status", "op": "eq",
"value": "deprecated" }
}
}
10. AI Namespace — Semantic Tags
10.1 — Purpose
Enables AI agents to annotate events with semantic metadata derived from decrypting and processing the payload locally — without the store ever seeing the content. The resulting semantic tags are stored as unencrypted annotations, making them queryable without re-processing.
This creates a persistent semantic index — AI-derived labels that make encrypted content discoverable by meaning rather than just by metadata.
10.2 — Fields
| Field | Type | Auth | Mutable | Indexed | Description |
|---|---|---|---|---|---|
ai.intent |
string | AUTHORIZED_KEYS | Yes | Yes | Detected intent (e.g., purchase-inquiry, support-request) |
ai.sentiment |
enum | AUTHORIZED_KEYS | Yes | Yes | positive, neutral, negative, mixed |
ai.entities |
array |
AUTHORIZED_KEYS | Yes | Yes | Named entities extracted from content |
ai.topics |
array |
AUTHORIZED_KEYS | Yes | Yes | Topic labels |
ai.language |
string (BCP-47) | AUTHORIZED_KEYS | Yes | Yes | Detected language code |
ai.summary |
string | AUTHORIZED_KEYS | Yes | No | Short human-readable summary (not stored in index) |
ai.embedding_ref |
string (SHA-256) | AUTHORIZED_KEYS | Yes | No | Reference to a separately stored embedding vector blob |
ai.model |
string | AUTHORIZED_KEYS | Yes | No | Model identifier that produced these tags |
ai.confidence |
float | AUTHORIZED_KEYS | Yes | No | Confidence score for the annotations (0.0–1.0) |
ai.processed_at |
integer (unix ms) | AUTHORIZED_KEYS | Yes | No | When the AI processing occurred |
10.3 — Security Note and AI Annotator Safety Profile
AI annotations are produced by decrypting the payload locally on an authorized device and then annotating the store with derived semantic tags. The store never receives the decrypted payload. Only the derived tags — which must not contain sensitive verbatim content — are stored as annotations.
The categorical vs extractive rule: This is the core safety
constraint for AI annotators. Every ai.* field must be categorical
(a label from a defined or bounded vocabulary) rather than extractive
(a substring, paraphrase, or reconstruction of source content). The
distinction:
| Safe — Categorical | Unsafe — Extractive |
|---|---|
ai.intent: "purchase-inquiry" |
ai.summary: "Alice wants 500 units of Product X by Friday" |
ai.sentiment: "positive" |
ai.entities: ["Alice Johnson", "500", "Product X"] |
ai.topics: ["pricing", "delivery"] |
ai.summary: "Customer inquiring about bulk discount on Q2 order" |
ai.entities: ["acme-corp"] |
ai.entities: ["alice@acme-corp.com"] |
The rule in one sentence: if removing the annotation would allow a reader to reconstruct any specific fact from the payload, the annotation is extractive and must not be stored.
Recommended AI Annotator Safety Profile:
Implementations that produce ai.* annotations should enforce the
following constraints before writing:
- No proper nouns from source content in
ai.entitiesunless they are organization names that appear in a known entity registry (i.e., the entity was recognized, not extracted verbatim). - No numeric values from source content (prices, quantities, dates,
identifiers, phone numbers) in any
ai.*field. - No substrings longer than 32 characters from source content in
any
ai.*field. ai.summarymust be abstractive, not extractive. It must be a re-expression in different words, not a selection of phrases from the source. If the annotator cannot produce a fully abstractive summary,ai.summarymust be omitted.ai.intentandai.topicsmust draw from a declared controlled vocabulary defined in the annotation schema. Open-ended string values for these fields are discouraged; enum types are preferred.ai.entitiesvalues must be normalized (lowercase, no punctuation, canonical form) to prevent fingerprinting through capitalization or punctuation patterns that match source content.
These constraints are recommendations, not protocol enforcement — the store cannot verify them because it never sees the payload. Annotator implementations are responsible for applying them.
ai.embedding_ref special case: An embedding vector stored as a
separately encrypted blob (referenced by SHA-256 hash) may encode
semantic information in a form that resists extraction. Embedding blobs
should be encrypted to the same key set as the source event. The
ai.embedding_ref field stores only the content hash — never the vector
itself in plaintext.
10.4 — Query Patterns
// All events with purchase-inquiry intent from the last 30 days
{
"kind": "xp.store.query",
"payload": {
"filter": {
"and": [
{ "field": "annotations.ai.intent", "op": "eq", "value": "purchase-inquiry" },
{ "field": "timestamp", "op": "gte", "value": 1714408000 }
]
}
}
}
// Events mentioning a specific entity
{
"kind": "xp.store.query",
"payload": {
"filter": {
"field": "annotations.ai.entities",
"op": "contains",
"value": "acme-corp"
}
}
}
11. Custom Namespace — Application Extensions
11.1 — Purpose
Applications declare their own annotation fields under the custom
namespace without conflicting with the standard namespaces. Custom
annotations follow the same authorization, mutability, and indexing
model as standard annotations.
11.2 — Declaration
Custom annotations are declared in an Annotation Schema event (§12) before use:
{
"kind": "xp.graph.annotation-schema.define",
"payload": {
"namespace": "custom.crm",
"applies_to": ["salesforce.opportunity.*", "acme.order.*"],
"annotations": {
"custom.crm.opportunity_id": {
"type": "string",
"auth": "SENDER_ONLY",
"mutable": false,
"indexed": true,
"description": "Salesforce Opportunity ID linked to this event"
},
"custom.crm.deal_stage": {
"type": "enum",
"values": ["prospecting", "qualified", "proposal", "negotiation", "closed"],
"auth": "AUTHORIZED_KEYS",
"mutable": true,
"indexed": true
}
}
}
}
12. Annotation Schema System
12.1 — Purpose
Annotation schemas are machine-readable contracts that define which
annotation fields are valid for a given event kind, who can write them,
whether they are mutable, and whether they are indexed. The store enforces
annotation schemas on every xp.graph.annotate write.
12.2 — Schema Definition Event
{
"kind": "xp.graph.annotation-schema.define",
"payload": {
"schema_id": "xp.message.direct.annotations.v1",
"applies_to": "xp.message.direct",
"annotations": {
"thread.thread_id": {
"type": "string",
"auth": "PARTICIPANTS",
"mutable": false,
"indexed": true
},
"recipient.read": {
"type": "boolean",
"auth": "RECIPIENT_ONLY",
"mutable": true,
"indexed": true
},
"recipient.folder": {
"type": "string",
"auth": "RECIPIENT_ONLY",
"mutable": true,
"indexed": true
},
"trace.trace_id": {
"type": "string",
"auth": "SENDER_ONLY",
"mutable": false,
"indexed": true
},
"groups": {
"type": "array<group_ref>",
"auth": "PARTICIPANTS",
"mutable": true,
"indexed": true
},
"links": {
"type": "array<link_ref>",
"auth": "PARTICIPANTS",
"mutable": true,
"indexed": true
},
"ai.intent": {
"type": "string",
"auth": "AUTHORIZED_KEYS",
"mutable": true,
"indexed": true
}
}
}
}
12.3 — Authorization Levels
| Level | Who Can Write |
|---|---|
SENDER_ONLY |
Only the original event sender's key |
RECIPIENT_ONLY |
Only the original event recipient's key |
PARTICIPANTS |
Either the sender or any recipient |
AUTHORIZED_KEYS |
Keys explicitly granted annotation authority for this field |
STORE_MANAGED |
Only the store itself (e.g., delivery status, superseded_by) |
ANY_VERIFIED |
Any key with a verified signature (public annotations) |
12.4 — Schema Inheritance
Annotation schemas support inheritance. A schema for xp.message.*
declares common fields that apply to all message subtypes. A schema for
xp.message.direct inherits those fields and may add its own:
{
"kind": "xp.graph.annotation-schema.define",
"payload": {
"schema_id": "xp.message.annotations.v1",
"applies_to": "xp.message.*",
"inherits": [],
"annotations": { ... }
}
}
13. The xp.graph.* Schema Family
All Graph Store operations use the xp.graph.* namespace. The
xp.store.* namespace handles base event storage and retrieval; the
xp.graph.* namespace handles annotation management and graph traversal.
13.1 — Schema Inventory
xp.graph.annotate — add or update annotations on a stored event
xp.graph.annotate.bulk — annotate multiple events in one operation
xp.graph.annotation.history — retrieve the full annotation change history for an event
xp.graph.annotation-schema.define — declare the annotation schema for an event kind
xp.graph.annotation-schema.get — retrieve an annotation schema
xp.graph.traverse — traverse the event graph by following link edges
xp.graph.trace.view — return pre-computed DAG/waterfall data for a trace_id
xp.graph.group.define — create a new named group
xp.graph.group.get — retrieve a group definition
xp.graph.group.list — list all groups accessible to the requesting key
xp.graph.group.membership.add — add an entity to a group
xp.graph.group.membership.remove — remove an entity from a group
13.2 — xp.graph.annotate
The primary write operation. Adds or updates annotation fields on a stored event.
{
"kind": "xp.graph.annotate",
"payload": {
"target_event_id": "sha256-abc123",
"operation": "merge",
"annotations": {
"thread.thread_id": "thread-xyz789",
"thread.depth": 0,
"thread.position": 1,
"groups": [
{ "group_id": "project-phoenix", "group_type": "project" }
],
"recipient.read": false,
"trace.trace_id": "trace-7f3a9c",
"trace.span_id": "span-2b1f"
}
}
}
Operations:
| Operation | Semantics |
|---|---|
set |
Set a specific annotation key to a value (replaces existing) |
merge |
Merge a dictionary of annotations (each key replaces its prior value) |
append |
Append to an array annotation (groups, links, external_refs) |
remove |
Remove a specific key from the annotation envelope |
remove_from |
Remove a specific item from an array annotation |
The store validates:
1. The annotating key's signature
2. The annotating key's authorization level for each field being set
3. Compliance with the declared annotation schema for this event kind
4. Immutability constraints (cannot set a field marked mutable: false twice)
13.3 — xp.graph.traverse
Graph traversal query — follows link edges rather than filtering by field.
{
"kind": "xp.graph.traverse",
"payload": {
"start_event_id": "sha256-root",
"direction": "outbound",
"rel_filter": ["caused_by", "part_of", "reply_to"],
"max_depth": 5,
"include_annotations": true,
"page_size": 50
}
}
Response:
{
"kind": "xp.graph.traverse.result",
"payload": {
"nodes": [
{
"event": "<XpEvent encrypted>",
"annotations": { ... },
"depth": 0,
"incoming_rel": null
},
{
"event": "<XpEvent encrypted>",
"annotations": { ... },
"depth": 1,
"incoming_rel": "caused_by"
}
],
"edges": [
{ "from": "sha256-root", "to": "sha256-child", "rel": "caused_by" }
],
"truncated": false
}
}
13.4 — xp.graph.annotation.history
Every annotation change is a signed event stored in the Graph Store. This operation retrieves the full mutation history for a specific annotation field on a specific event.
{
"kind": "xp.graph.annotation.history",
"payload": {
"target_event_id": "sha256-abc123",
"annotation_key": "workflow.current_step"
}
}
Response includes each mutation: the value before and after, the key that made the change, and the timestamp — all cryptographically verifiable from the stored annotation events.
14. What the Graph Store Replaces
A single XProtocol Graph Store deployment answers queries that currently require assembling and integrating multiple distinct infrastructure components:
| Traditional Stack | Graph Store Equivalent | Security Improvement |
|---|---|---|
| NoSQL document store | Permanent encrypted event storage + metadata queries | Cryptographic access control; provable authorship |
| Graph database (Neo4j) | Typed link annotations + xp.graph.traverse |
Identity-native; every edge is authorized and auditable |
| Distributed tracing (Jaeger, Zipkin, OTel) | trace.* annotation namespace |
Tamper-evident; every span is signed by its producing system |
| Message queue (Kafka, Kinesis) | xp.store.subscribe with historical replay |
End-to-end encrypted; identity-native |
| Email inbox (IMAP folder model) | recipient.* annotation namespace |
Per-recipient view without shared-state server |
| Workflow engine (Temporal, Airflow) | workflow.* annotation namespace |
State machine on events; no separate workflow database |
| Audit log (Splunk, ELK) | Event store + annotation history | Tamper-evident by construction; every record is signed |
| Version control for records | versioning.* annotation namespace |
Immutable history; cryptographic proof of change chain |
| AI memory / semantic search | ai.* annotation namespace + embedding refs |
Persistent semantic index without re-processing |
| Cross-system foreign keys | external_refs in links namespace |
Universal foreign key across all integrated services |
| Observability platform | trace.* + xp.store.stats |
No collector infrastructure; cryptographically verifiable |
15. Implementation Requirements
15.1 — Required Capabilities
A Graph Store implementation must support:
- The
xp.store.*schema family (base event store — persistent retention,xp.store.query,xp.store.get,xp.store.subscribe,xp.store.stats) - The
xp.graph.annotateoperation with all five operations (set, merge, append, remove, remove_from) - Annotation schema enforcement (authorization, mutability, type checking)
- Index maintenance for all fields declared
indexed: true xp.graph.annotation.historyfor any annotated field- Storage of annotation events as first-class events in the store
15.2 — Optional Capabilities
Implementations may additionally support:
xp.graph.traversefor link-following graph queriesxp.graph.group.*operations- AI annotation processing pipeline
- TEE-based payload-level queries
15.3 — Capability Declaration
Implementations declare their Graph Store capabilities in their
xp.endpoint.announce event:
"capabilities": [
"xp.store.*",
"xp.graph.annotate",
"xp.graph.annotation.history",
"xp.graph.traverse",
"xp.graph.group.*"
]
16. Security Considerations
16.1 — Annotation Exposure
Annotations are unencrypted by design — they must be queryable by the store without payload decryption. This means annotations must never contain sensitive information. The rule: if you wouldn't want the store operator to see it, it belongs in the encrypted payload, not in an annotation.
16.2 — Annotation Forgery Prevention
Every xp.graph.annotate event is signed by the annotating key. The
store validates the signature before applying the annotation. Annotation
events are stored alongside the base events — they cannot be silently
backdated or forged.
16.3 — AI Annotation Leakage Risk
AI annotations (§10) are derived from decrypted payload content and stored as plaintext. Implementations must ensure that AI-derived tags do not inadvertently reconstruct sensitive payload content through over-specific entity extraction, verbatim phrases in summaries, or highly specific topic labels.
16.4 — Authorization Enforcement
The store must enforce annotation authorization levels
(SENDER_ONLY, RECIPIENT_ONLY, PARTICIPANTS, AUTHORIZED_KEYS, STORE_MANAGED)
on every write. An authorization failure returns xp.error with code
AUTHORIZATION_INSUFFICIENT and does not apply any part of the annotation.
17. Relationship to Other XProtocol Extensions
The Graph Store builds directly on the Event Store (xp.store.*) and
the core XProtocol event model. It is used by:
- XProtocol-Native Environment Model — environment state, capability
grants, and deployment authorizations are stored as graph events,
queryable via the
groupsandtracenamespaces - XProtocol MCP Adapter — AI agents interact with the graph store
through MCP tools exposing
xp.graph.annotateandxp.graph.traverse - XProtocolChat — message threading, inbox state, and conversation history sync are natural graph store applications beginning in V2
18. Schema Governance and Evolution
18.1 — Who Can Propose
Any party may propose a new community annotation schema or modification
to an existing one by publishing an xp.graph.annotation-schema.propose
event addressed to the community governance endpoint. A proposal must
include: the schema definition, a rationale statement, backward
compatibility analysis, and at least one reference implementation.
Vendor-owned namespaces (e.g., salesforce.annotations.*) require no
community approval — the DNS-owning entity controls them entirely. Only
schemas in the xp.* reserved namespace and the org.xp-community.*
community namespace require ratification.
18.2 — Ratification Process
The community ratification process follows a lightweight RFC model:
| Phase | Duration | Criterion to Advance |
|---|---|---|
| Draft | Open-ended | Proposal published; at least one implementation exists |
| Comment | 30 days minimum | Proposal announced to community; comments collected |
| Candidate | 60 days | No blocking objections; two independent implementations pass interop suite |
| Ratified | Permanent | Codified in the specification; assigned a version number |
A blocking objection is one that identifies a correctness problem, a security vulnerability, or an irreconcilable conflict with an existing ratified schema. Style objections and feature requests do not block ratification.
18.3 — Breaking Changes and Forking
Minor versions add optional fields or new valid enum values. They are non-breaking. Implementations must silently ignore unknown fields per the extensibility rule. Minor versions do not require re-ratification.
Major versions may remove fields, change field types, or change semantics. They are breaking. A major version is a new schema that coexists with prior major versions — they are not replaced, only superseded for new usage. Services declare which major versions they support in their capability announcement.
Forking: If a major vendor diverges from a community schema in an
irreconcilable way, both versions coexist. The fork is documented with
an xp.graph.annotation-schema.fork event naming the divergence point,
the forking party's namespace, and the semantic differences. Neither
fork is authoritative over the other; market adoption determines
dominance. The protocol accommodates forks — it does not prevent them.
18.4 — Backward Compatibility Promise
Ratified schemas carry the following promises:
- Minor versions: All implementations supporting version N must accept events annotated with any version N.x without error.
- Major versions: Implementations must support at minimum the current major version and one prior major version simultaneously.
- Removal: Fields are never removed from a minor version. Removal requires a major version increment.
- Deprecation path: A field scheduled for removal must be marked
deprecated: truein the schema for at least one full major version before removal.
19. Environment Model Integration
The XProtocol-native environment model (described in
XProtocol-Strategic-Concepts.md Concept 2) uses the Graph Store as
its state backing. This section specifies the integration points.
19.1 — Environment as a Group
Each logical environment is represented as a named group in the Graph Store:
{
"kind": "xp.graph.group.define",
"payload": {
"group_id": "env-feature-payments-v2",
"group_type": "environment",
"display_name": "feature/payments-v2",
"owner_key": "<developer_key>",
"authorized_members": ["<developer_key>", "<ci_key>"],
"metadata": {
"inherits": "env-dev",
"data_scope": "synthetic",
"expires_at": 1717776000
}
}
}
Every event produced within an environment is annotated with the environment group ID:
"annotations": {
"groups": [{ "group_id": "env-feature-payments-v2", "group_type": "environment" }],
"trace": { "trace_id": "trace-xyz", "session_id": "sess-abc" }
}
This makes the entire event history of an environment queryable with a single filter, and makes cross-environment queries (e.g., "show me the same operation across dev and prod") trivial.
19.2 — Capability Inheritance: Intersection Semantics
Child environments inherit capabilities from parent environments using set intersection — never union. The effective capability set of a key in a child environment is:
effective_capabilities = parent_capabilities ∩ child_capabilities
A key can never gain capabilities through a child environment that its parent environment does not grant. This is the same model as JWT scope narrowing: you can always restrict inherited capabilities, never expand them.
Example:
parent env (dev): [read:synthetic, write:dev-schema, deploy:dev]
child override: [read:synthetic, write:dev-schema]
effective: [read:synthetic, write:dev-schema]
(deploy:dev is tightened out by the child)
19.3 — Capability Discovery
A key operating in an environment can query its own effective capabilities:
{
"kind": "xp.environment.capabilities.query",
"payload": {
"environment_id": "env-feature-payments-v2"
}
}
Response:
{
"kind": "xp.environment.capabilities.result",
"payload": {
"environment_id": "env-feature-payments-v2",
"requesting_key": "<key_fingerprint>",
"effective_capabilities": ["read:synthetic", "write:dev-schema"],
"inherited_from": "env-dev",
"expires_at": 1717776000,
"computed_at": 1717000000
}
}
The response is signed by the environment management service, not the store — it is an authoritative statement of what the requesting key is permitted to do. Clients cache this with a short TTL (recommended: 60 seconds) and re-query before any capability-sensitive operation.
19.4 — Data Classification via xp.data.classify
Events and records carry their data classification as a standard annotation field:
"annotations": {
"data_classification": {
"level": "synthetic",
"scope": "env-feature-payments-v2",
"set_by": "<ci_key_fingerprint>",
"set_at": 1717000000
}
}
Standard classification levels (ordered by sensitivity):
| Level | Description | Accessible in |
|---|---|---|
synthetic |
Generated test data; no real user information | All environments |
internal |
Real operational data not containing PII | Dev, staging, prod |
confidential |
Business-sensitive; restricted access | Staging, prod |
prod-pii |
Contains personal identifiable information | Prod only |
prod-financial |
Financial records, payment data | Prod only with audit |
The store enforces classification access at query time: a key in a
synthetic-scoped environment cannot retrieve events annotated
prod-pii, even if it would otherwise have recipient-based access.
Classification enforcement is a mandatory capability of any environment-
aware Graph Store implementation.
20. Relay Reputation and High-Trust Relays (Optional Extension)
This section describes an optional extension for relay reputation signaling. It is not part of the core Graph Store specification and is not required for a conformant implementation.
20.1 — Reputation Signals
Relay operators may publish signed reputation signals as XProtocol events, creating a queryable reputation history in the Graph Store:
{
"kind": "xp.relay.reputation.report",
"payload": {
"relay_key": "<relay_public_key>",
"reporter_key": "<reporting_key>",
"period_start": 1717000000,
"period_end": 1717086400,
"events_relayed": 142847,
"delivery_rate": 0.9994,
"median_latency_ms": 34,
"spam_events_rejected": 12,
"uptime_fraction": 0.9998
}
}
These reports accumulate in the Graph Store and are queryable by any key evaluating a relay's trustworthiness.
20.2 — Stake-Based High-Trust Designation (Optional)
For ecosystems that want economic commitment from relay operators, an
optional staking primitive allows relay operators to post cryptographic
proof of stake against a public key. Relays that have posted stake and
maintained a high reputation score over a rolling 90-day window may
declare themselves high-trust in their xp.endpoint.announce event.
High-trust designation is not enforced by the protocol — it is a self-declaration that clients can verify by querying the relay's reputation history in the Graph Store. The value is client trust, not protocol privilege.
This primitive is intentionally left lightweight. Mandatory staking or economic gatekeeping of relay operation would deter the community relay operators and self-hosted deployments that are essential to the decentralized model. Staking is opt-in, not required.
Appendix A — Full Annotation Field Reference
| Namespace.Field | Type | Auth | Mutable | Indexed |
|---|---|---|---|---|
threads[*].thread_id |
string | PARTICIPANTS | No | Yes |
threads[*].root_event_id |
string | PARTICIPANTS | No | Yes |
threads[*].reply_to_event_id |
string | SENDER_ONLY | No | Yes |
threads[*].depth |
integer | PARTICIPANTS | No | Yes |
threads[*].position |
integer | PARTICIPANTS | No | Yes |
threads[*].subject |
string | PARTICIPANTS | Yes | Yes |
threads[*].closed |
boolean | PARTICIPANTS | Yes | Yes |
recipient.read |
boolean | RECIPIENT_ONLY | Yes | Yes |
recipient.read_at |
integer | RECIPIENT_ONLY | Yes | No |
recipient.starred |
boolean | RECIPIENT_ONLY | Yes | Yes |
recipient.archived |
boolean | RECIPIENT_ONLY | Yes | Yes |
recipient.folder |
string | RECIPIENT_ONLY | Yes | Yes |
recipient.labels |
array |
RECIPIENT_ONLY | Yes | Yes |
recipient.snoozed_until |
integer | RECIPIENT_ONLY | Yes | Yes |
recipient.priority |
enum | RECIPIENT_ONLY | Yes | Yes |
recipient.note |
string | RECIPIENT_ONLY | Yes | No |
trace.trace_id |
string | SENDER_ONLY | No | Yes |
trace.span_id |
string | SENDER_ONLY | No | Yes |
trace.parent_span_id |
string | SENDER_ONLY | No | Yes |
trace.causation_id |
string | SENDER_ONLY | No | Yes |
trace.correlation_id |
string | SENDER_ONLY | No | Yes |
trace.saga_id |
string | SENDER_ONLY | No | Yes |
trace.session_id |
string | SENDER_ONLY | No | Yes |
trace.batch_id |
string | SENDER_ONLY | No | Yes |
trace.job_id |
string | SENDER_ONLY | No | Yes |
trace.request_id |
string | SENDER_ONLY | No | Yes |
groups[*].group_id |
string | PARTICIPANTS | Yes | Yes |
groups[*].group_type |
string | PARTICIPANTS | Yes | Yes |
links[*].event_id |
string | PARTICIPANTS | Yes | Yes |
links[*].rel |
string | PARTICIPANTS | Yes | Yes |
links[*].direction |
string | PARTICIPANTS | Yes | No |
external_refs[*].system |
string | PARTICIPANTS | Yes | Yes |
external_refs[*].entity |
string | PARTICIPANTS | Yes | Yes |
external_refs[*].id |
string | PARTICIPANTS | Yes | Yes |
workflow.workflow_id |
string | PARTICIPANTS | No | Yes |
workflow.instance_id |
string | PARTICIPANTS | No | Yes |
workflow.current_step |
string | AUTHORIZED_KEYS | Yes | Yes |
workflow.status |
enum | AUTHORIZED_KEYS | Yes | Yes |
workflow.assigned_to |
string | AUTHORIZED_KEYS | Yes | Yes |
workflow.steps_completed |
array |
AUTHORIZED_KEYS | Yes | No |
workflow.steps_remaining |
array |
AUTHORIZED_KEYS | Yes | No |
workflow.due_at |
integer | AUTHORIZED_KEYS | Yes | Yes |
workflow.started_at |
integer | STORE_MANAGED | No | Yes |
workflow.completed_at |
integer | STORE_MANAGED | No | Yes |
versioning.version |
integer | SENDER_ONLY | No | Yes |
versioning.entity_id |
string | SENDER_ONLY | No | Yes |
versioning.supersedes |
array |
SENDER_ONLY | No | Yes |
versioning.superseded_by |
string | STORE_MANAGED | Yes | Yes |
versioning.is_current |
boolean | STORE_MANAGED | Yes | Yes |
versioning.status |
enum | AUTHORIZED_KEYS | Yes | Yes |
versioning.changelog |
string | SENDER_ONLY | No | No |
versioning.major |
boolean | SENDER_ONLY | No | Yes |
ai.intent |
string | AUTHORIZED_KEYS | Yes | Yes |
ai.sentiment |
enum | AUTHORIZED_KEYS | Yes | Yes |
ai.entities |
array |
AUTHORIZED_KEYS | Yes | Yes |
ai.topics |
array |
AUTHORIZED_KEYS | Yes | Yes |
ai.language |
string | AUTHORIZED_KEYS | Yes | Yes |
ai.summary |
string | AUTHORIZED_KEYS | Yes | No |
ai.embedding_ref |
string | AUTHORIZED_KEYS | Yes | No |
ai.model |
string | AUTHORIZED_KEYS | Yes | No |
ai.confidence |
float | AUTHORIZED_KEYS | Yes | No |
ai.processed_at |
integer | AUTHORIZED_KEYS | Yes | No |
data_classification.level |
enum | AUTHORIZED_KEYS | Yes | Yes |
data_classification.scope |
string | AUTHORIZED_KEYS | Yes | Yes |
data_classification.set_by |
string | AUTHORIZED_KEYS | Yes | No |
data_classification.set_at |
integer | AUTHORIZED_KEYS | Yes | No |
XProtocol.ai is an independent open protocol project and is not affiliated with, endorsed by, or connected to XProtocol.org or any related entities.