Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
13 KiB
NIP-AP
Agent Personas
draft optional
This NIP defines kind:30175 persona events — public, addressable definitions that describe how to instantiate an AI agent. A persona carries identity (display name, avatar), behavioral configuration (system prompt, model, runtime), and an optional name pool. It is the "blueprint" from which agents are spawned.
Kind
This NIP claims kind:30175 for agent persona definitions. It is in the NIP-33 parameterized replaceable range (30000–39999) per NIP-01: addressed by (pubkey, kind, d_tag), with only the latest event per address retained.
A dedicated kind (rather than encoding personas as NIP-78 kind:30078 "Application-specific Data") is taken for the same reasons as NIP-AE: (1) it isolates this NIP's address space from any other application using the same pubkey — persona slugs cannot collide with another app's d tag choices; (2) it lets observers, indexers, and unknown-kind viewers identify persona events from the kind alone, without parsing content as a namespace demultiplexer.
Roles
- owner — a Nostr identity (
pubkey_o) that publishes and manages persona definitions. Typically the workspace operator. - agent — a Nostr identity instantiated from a persona. Agents do NOT author persona events; they consume them. An agent MAY store a private snapshot of its originating persona in a NIP-AE engram at
mem/persona(encrypted, owner-readable).
Slugs
The d tag of a persona event is the plaintext persona slug. A valid slug matches:
^[a-z0-9][a-z0-9_-]{0,63}$
Total length: 1–64 bytes. Slugs are flat identifiers (no path separators), unlike NIP-AE memory slugs which are hierarchical (mem/…).
Plaintext rationale
The d-tag is deliberately NOT blinded (contrast with NIP-AE which HMAC-blinds d-tags to protect memory slug confidentiality). Personas are public definitions meant for discovery:
- Direct filter queries:
{kinds: [30175], authors: [pubkey], "#d": ["my-persona"]} - Human-readable addressing in UIs
- Cross-workspace sharing without a shared secret
Event envelope
{
"kind": 30175,
"pubkey": "<pubkey_o>",
"created_at": <unix_seconds>,
"tags": [
["d", "<persona-slug>"]
],
"content": "<json_body>"
}
There MUST be exactly one d tag and it MUST contain a valid slug per the grammar above. The relay enforces this constraint on ingest. There is no p tag — persona events are owner-to-self definitions, not directed at a counterparty.
Implementations MAY include a NIP-31 ["alt", "agent persona definition"] tag to give unknown-kind viewers a non-leaking summary. Additional tags beyond d and alt are not defined by this NIP and have no effect on validity.
Content body
The content field is a plaintext (unencrypted) JSON object:
{
"display_name": "<string>",
"system_prompt": "<string>",
"avatar_url": "<string | null>",
"runtime": "<string | null>",
"model": "<string | null>",
"provider": "<string | null>",
"name_pool": ["<string>", ...]
}
Required fields
| Field | Type | Description |
|---|---|---|
display_name |
string | Human-readable name for the persona. |
system_prompt |
string | The system prompt injected into agent sessions. |
Optional fields
| Field | Type | Default | Description |
|---|---|---|---|
avatar_url |
string | null | null |
URL to an avatar image. |
runtime |
string | null | null |
ACP runtime identifier (e.g. "goose", "claude-code"). |
model |
string | null | null |
Model identifier (e.g. "claude-opus-4"). |
provider |
string | null | null |
Model provider (e.g. "anthropic"). |
name_pool |
string[] | [] |
Pool of display names for agent instances spawned from this persona. When non-empty, the spawning system picks a name from this pool for each new agent instance, enabling multiple concurrent agents from the same persona to have distinct identities. |
Unknown fields MUST be ignored by readers (forward compatibility).
Prohibited: secrets in content
The content body is public and unencrypted. It MUST NOT contain secrets (API keys, tokens, credentials, or any sensitive environment variables). In particular, an env_vars field MUST NOT appear in the content body.
Secrets required by agents spawned from a persona MUST be conveyed through a separate encrypted channel — specifically, the NIP-AE engram at mem/persona (which is NIP-44 encrypted to the agent↔owner conversation key) or through out-of-band injection at spawn time.
Encryption rationale
Persona events carry no encryption. This is deliberate:
- Personas are configuration, not state. They describe what an agent should be, not what it has learned.
- Encryption would prevent relay-side indexing, search, and third-party client rendering — all desirable for definitions that workspace members should browse.
- Operators who need confidentiality should use relay-level access control (NIP-42 authentication + NIP-29 group membership) rather than event-level encryption.
Replacement semantics
Standard NIP-33: for a given (pubkey, kind:30175, d_tag), only the event with the greatest created_at is the head. Ties are broken by lowest event id per NIP-01. Relays SHOULD return only the head; clients MUST select the head from any multi-event response.
Writing
To write or update a persona with slug s and body b:
- Validate
sagainst the slug grammar. Reject if invalid. - Serialize
bto JSON. Reject if the serialized body exceeds 65,535 bytes. - Compute the head of
sper NIP-33 and letTbe itscreated_at(or 0 if no head exists). Setcreated_at := max(now, T + 1). Monotonicity ensures fresh writes always supersede prior heads regardless of clock skew. - Tags:
[["d", s]]. - Sign with
seckey_oand publish to configured relays.
Reading
To read a single persona by slug s:
Filter: {kinds: [30175], authors: [pubkey_o], "#d": [s]}
Select the head per NIP-33 rules. Parse content as JSON. Validate required fields.
To list all personas for an owner:
Filter: {kinds: [30175], authors: [pubkey_o]}
Returns all heads. Clients scope by author pubkey — two different owners MAY publish personas with the same slug; these are independent events.
Deletion
Owners MAY publish NIP-09 deletion requests targeting persona events. A deletion request MUST be authored by the same key (pubkey_o). Such requests SHOULD include ["k", "30175"] and use an a-tag identifier 30175:<pubkey_o>:<slug>.
A subsequent write with a later timestamp resurrects the slug under NIP-33 replacement semantics.
Relationships to other NIPs
NIP-AE (Agent Engrams)
Agents spawned from a persona MAY store a private snapshot at the reserved engram slug mem/persona. This engram:
- Is NIP-44 encrypted (confidential to agent + owner)
- MAY contain secrets (env vars, API keys) that the public persona event must not carry
- Serves as the agent's private, mutable copy of its originating configuration
- References back to the persona event by slug convention, not by event ID
The mem/persona slug conforms to NIP-AE's slug grammar and requires no amendment to that spec.
NIP-OA (Owner Attestation)
Agents spawned from a persona carry NIP-OA owner attestation — an auth tag proving that pubkey_o authorized the agent's key. The persona event itself does not contain attestation; it is the definition from which attestation is issued at spawn time.
Relay behavior
- The relay MUST accept
kind:30175events that pass standard NIP-33 validation (valid signature, exactly onedtag with a non-empty value). - The relay stores persona events globally (
channel_id = NULL); they are not channel-scoped. - The relay is NOT required to validate that
contentparses as validPersonaEventContentJSON. Relays are dumb stores per Nostr convention; content validation is a client responsibility. - The relay MUST enforce that the
dtag is non-empty (standard NIP-33 requirement for parameterized replaceable events).
Security considerations
- No encryption. System prompts, model names, runtime identifiers, and all configuration are visible to anyone with relay read access. Operators MUST NOT store secrets in persona event content.
- System prompt sensitivity. System prompts may contain security-relevant behavioral instructions. Publishing them unencrypted enables adversarial prompt extraction. Operators who consider system prompts confidential SHOULD NOT publish them in persona events, or SHOULD use a relay with appropriate access controls.
- Write authority. Only the holder of
seckey_ocan publish or replace persona events. NIP-33 replacement is scoped by pubkey — no spoofing risk from other relay members. - Slug collision across pubkeys. Two different owners can publish personas with the same slug. Clients MUST always scope queries by author pubkey, not just slug.
- Metadata exposure. The
(pubkey, kind:30175, slug)triple reveals persona existence. Event timestamps reveal edit history. - No owner write authority over agents. Persona events define what an agent should be; they do not grant runtime control over a running agent. The agent consumes the persona at spawn time. Updates to the persona event do not automatically propagate to running agents.
Reference test vectors
TEST KEYS — DO NOT USE IN PRODUCTION. The keys below are pinned for reproducibility. Production code MUST source randomness from a CSPRNG.
Inputs
seckey_o = 0000000000000000000000000000000000000000000000000000000000000001
schnorr_aux = 0000000000000000000000000000000000000000000000000000000000000000
Derived
pubkey_o = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Event 1 — create persona with all fields
// Body (exact UTF-8, no trailing whitespace):
{"display_name":"Test Agent","system_prompt":"You are a test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta"]}
kind = 30175
pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
created_at = 1700000000
tags = [["d", "test-agent"]]
content = {"display_name":"Test Agent","system_prompt":"You are a test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta"]}
id = <derived per NIP-01: sha256([0, pubkey, created_at, kind, tags, content])>
sig = <BIP-340 Schnorr signature with aux=0x00…00>
Event 2 — minimal persona (required fields only)
// Body:
{"display_name":"Minimal","system_prompt":"Hello."}
kind = 30175
pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
created_at = 1700000001
tags = [["d", "minimal"]]
content = {"display_name":"Minimal","system_prompt":"Hello."}
id = <derived per NIP-01>
sig = <BIP-340 Schnorr signature with aux=0x00…00>
Event 3 — replacement (same slug, higher created_at)
// Updated body (system_prompt changed):
{"display_name":"Test Agent","system_prompt":"You are an updated test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta","Gamma"]}
kind = 30175
pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
created_at = 1700000002
tags = [["d", "test-agent"]]
content = {"display_name":"Test Agent","system_prompt":"You are an updated test assistant.","avatar_url":"https://example.com/avatar.png","runtime":"goose","model":"claude-opus-4","provider":"anthropic","name_pool":["Alpha","Beta","Gamma"]}
id = <derived per NIP-01>
sig = <BIP-340 Schnorr signature with aux=0x00…00>
After Event 3, the head for slug test-agent is Event 3 (greatest created_at). Event 1 is superseded.
Head selection with tiebreak
If two events share created_at = 1700000000 and slug test-agent, the head is the event with the lexicographically lowest id (hex comparison per NIP-01).
Implementation notes
Unlike NIP-AE, persona events involve no encryption, no HMAC derivation, and no conversation key. The test vectors are standard NIP-33 events with JSON content — implementations need only:
- Correct NIP-01 event-id serialization:
json.dumps([0, pubkey, created_at, kind, tags, content], separators=(",", ":"), ensure_ascii=False)over UTF-8 bytes. - BIP-340 Schnorr signing with the pinned aux value.
- JSON serialization of the content body with no trailing whitespace or BOM.