Who do I know, and how well? Without a social graph an agent cannot tell a known contact from a stranger, so every interaction starts from zero.
Social graph manager for AI agents, contacts, trust tiers, conversation evaluation, guardrails, cross-channel recognition, and identity verification. All anchored to Nostr npub identity.
Minimal dependencies: bech32 only. No heavyweight crypto libraries required.
Create an enclave, add a contact, get tier-based behavior rules.
from nostrsocial import SocialEnclave, Tier # Create an enclave and back up the device secret immediately enclave = SocialEnclave.create() secret = enclave.export_secret() # Store this securely # Add a contact and get tier-based behavior enclave.add("alice@example.com", "email", Tier.CLOSE, display_name="Alice") rules = enclave.get_behavior("alice@example.com", "email") # rules.warmth=0.8, rules.token_budget=1500, rules.can_interrupt=True
Add contacts to friends, block, or gray lists with capacity enforcement.
from nostrsocial import SocialEnclave, Tier enclave = SocialEnclave.create() enclave.add("alice@example.com", "email", Tier.CLOSE, display_name="Alice") enclave.block("spam@bad.com", "email") enclave.gray("meh@example.com", "email")
Get tier-based behavioral parameters for any contact. Unknown contacts get neutral behavior.
rules = enclave.get_behavior("alice@example.com", "email") # rules.token_budget, rules.warmth, rules.can_interrupt, etc. # Unknown contacts get neutral behavior rules = enclave.get_behavior("stranger@example.com", "email") # warmth=0.5, token_budget=500
Combine WHO someone is with WHAT is happening to determine HOW to respond. Pass ConversationSignals and get back an Evaluation with adjusted warmth, token budget, approach guidance, and a recommended action.
from nostrsocial import ConversationSignals signals = ConversationSignals( sentiment="vulnerable", vulnerability=0.7, reciprocity=0.8, engagement=0.9, topic_depth=0.6, ) result = enclave.evaluate("alice@example.com", "email", signals) # result.action = Action.HOLD # result.approach = "full presence" # result.adjusted_warmth = 0.96 # result.adjusted_token_budget = 1950
Screen conversation text for banned words, topics, and patterns. Returns a ScreenResult with severity, category, and recommended action.
result = enclave.screen("some incoming message text") if result.flagged: print(result.action) # "block", "exit", "warn", or "demote" print(result.severity) # 0.0-1.0 print(result.category) # "slurs", "manipulation", etc. print(result.matched) # "[slurs]" (never raw input) # Screen display names for known bad-actor patterns result = enclave.screen_entity("crypto_support_official")
Analyze the social graph and get a human-readable profile.
shape = enclave.network_shape() # shape.profile_type = "balanced", "fortress", "deep-connector", etc. # shape.narrative = "12 friends (2 intimate, 4 close, ...) # shape.tier_counts, shape.verified_count, shape.avg_interaction_days
Run drift detection, gray decay, and at-risk reporting in a single call. Use dry_run=True to preview changes without committing them.
# Preview what would happen preview = enclave.maintain(dry_run=True) print(preview["summary"]) # Execute maintenance for real result = enclave.maintain() # result["drifted"], result["decayed"], result["at_risk"], result["summary"]
Recognize the same person across different channels. This is resonance, not surveillance, it only checks contacts you already have a relationship with.
# Check if a new contact might be someone you already know matches = enclave.recognize( "alicedev", "twitter", display_name="Alice", ) for match in matches: print(f"{match.confidence}: {match.reason}") # Explicitly link two identities (never auto-linked) result = enclave.link( "alice@example.com", "email", "alicedev", "twitter", )
Track identity state from proxy to claimed to verified.
# See who needs verification for contact in enclave.get_upgradeable(): print(f"{contact.display_name}: {contact.upgrade_hint}") # Create a challenge for a claimed npub challenge = enclave.create_challenge("npub1alice...")
Save and load the social graph.
from nostrsocial import FileStorage storage = FileStorage("~/.agent/social.json") enclave = SocialEnclave.create(storage) enclave.add("alice@example.com", "email", Tier.CLOSE) enclave.save() # Later enclave = SocialEnclave.load(storage)
Friends list total: 150 (Dunbar's number). Unknown contacts get neutral behavior (warmth 0.5, budget 500).
| Tier | Slots | Warmth | Token Budget | Can Interrupt | Share Context | Proactive |
|---|---|---|---|---|---|---|
| INTIMATE | 5 | 0.95 | 2000 | Yes | Yes | Yes |
| CLOSE | 15 | 0.8 | 1500 | Yes | Yes | No |
| FAMILIAR | 50 | 0.6 | 1000 | No | No | No |
| KNOWN | 80 | 0.5 | 750 | No | No | No |
| BLOCK | 50 | 0.0 | 0 | No | No | No |
| GRAY | 100 | 0.2 | 200 | No | No | No |
Returned by add(), promote(), demote().
| Field | Type | Description |
|---|---|---|
| identifier | str | Email, phone, npub, etc. |
| channel | str | "email", "phone", "npub", "twitter" |
| list_type | ListType | FRIENDS, BLOCK, or GRAY |
| tier | Tier | None | INTIMATE, CLOSE, FAMILIAR, or KNOWN |
| identity_state | IdentityState | PROXY, CLAIMED, or VERIFIED |
| proxy_npub | str | HMAC-derived npub for non-npub contacts |
| display_name | str | None | Human-readable name |
| interaction_count | int | Total interactions recorded |
| upgrade_hint | str | Hint for identity verification |
Returned by get_behavior().
| Field | Type | Description |
|---|---|---|
| token_budget | int | Token allowance (intimate=2000, known=750, block=0) |
| memory_depth | int | Past interactions to consider |
| can_interrupt | bool | Can interrupt ongoing tasks |
| warmth | float | 0.0–1.0 (intimate=0.95, known=0.5, block=0.0) |
| response_priority | int | 1=highest (intimate), 10=block |
| share_context | bool | Share agent context with this contact |
| proactive_contact | bool | Agent initiates contact |
Returned by evaluate().
| Field | Type | Description |
|---|---|---|
| action | Action | HOLD, PROMOTE, DEMOTE, WATCH, BLOCK, or REACH_OUT |
| confidence | float | 0.0–1.0 |
| adjusted_warmth | float | Warmth for this specific moment |
| adjusted_token_budget | int | Token budget for this response |
| approach | str | "lean in", "de-escalate", "match energy", etc. |
| rationale | str | Why this recommendation |
| tier_suggestion | Tier | None | Suggested tier if promote/demote |
Returned by screen().
| Field | Type | Description |
|---|---|---|
| flagged | bool | Whether content was flagged |
| severity | float | 0.0–1.0 |
| category | str | "slurs", "hate_symbols", "manipulation", etc. |
| matched | str | Category tag like [slurs] (never raw input. PII safe) |
| action | str | "block", "exit", "warn", or "demote" |
| rationale | str | Human-readable explanation |
Returned by network_shape().
| Field | Type | Description |
|---|---|---|
| total_contacts | int | Total across all lists |
| tier_counts | dict | Per-tier counts |
| verified_count | int | Verified identities |
| profile_type | str | "balanced", "fortress", "deep-connector", etc. |
| narrative | str | Human-readable network description |
| Task | Module | Function |
|---|---|---|
| Add/remove contacts | enclave | add, block, gray, remove |
| Change trust tier | enclave | promote, demote |
| Get behavioral rules | enclave / behavior | get_behavior |
| Evaluate a conversation | enclave / evaluate | evaluate with ConversationSignals |
| Screen content | enclave / guardrails | screen, screen_entity |
| Check remaining slots | enclave | slots_remaining |
| Find unverified contacts | enclave | get_unverified_contacts, get_upgradeable |
| Create verification challenge | enclave / verify | create_challenge |
| Derive proxy npub | proxy | derive_proxy_npub |
| Run maintenance | enclave | maintain, maintain(dry_run=True) |
| Recognize cross-channel | enclave / resonance | recognize |
| Link two identities | enclave / resonance | link |
| Analyze network profile | enclave | network_shape |
| Displace weakest in tier | enclave | displace |
| Persist social graph | enclave | save / load |
export_secret() after create() and store it securely. If you lose it, all proxy npubs become unrecoverable.nostrkey package provides Identity.load() for this.[slurs] to prevent PII leakage in logs or downstream systems.data/*.json (banned words, topics, entities). Operators can override or extend via extra_words, extra_topics, and extra_entities parameters.All capacity constants are defined in types.py and can be overridden via SocialEnclave.create(tier_capacity=..., list_capacity=...).
| List | Capacity |
|---|---|
| INTIMATE | 5 |
| CLOSE | 15 |
| FAMILIAR | 50 |
| KNOWN | 80 |
| BLOCK | 50 |
| GRAY | 100 (auto-decay after 30 days) |
| Friends total | 150 (Dunbar's number) |
| State | Meaning |
|---|---|
| PROXY | HMAC-derived from email/phone/handle. Default for new contacts. |
| CLAIMED | User provided an npub but it has not been verified. |
| VERIFIED | Signed challenge confirms npub ownership. Verified contacts get warmer behavior. |
| Tier | Silent Days Before Demotion |
|---|---|
| INTIMATE | 30 days |
| CLOSE | 60 days |
| FAMILIAR | 90 days |
| KNOWN | 180 days (then moves to gray) |
NostrSocial works with Nostr identity but does not require relay connectivity for core operations. Proxy npubs are derived locally via HMAC-SHA256.
| NIP | Usage | Status |
|---|---|---|
| NIP-01 | Event signing for verification challenges | Stub (0.1.0) |
| NIP-02 | Contact list structure (Dunbar-inspired tiers) | Implemented |
| NIP-19 | bech32 npub encoding for proxy identities | Implemented |
NostrSocial is Pillar 4 of the NSE platform, the relationships layer. It works standalone or alongside the other OpenClaw skills.
| Pillar | Package | Domain |
|---|---|---|
| 1. Identity | nostrkey | Cryptographic keypairs, signing, encryption |
| 2. Finance | nostrwalletconnect | Lightning wallet access via NWC |
| 3. Time | nostrcalendar | Self-owned scheduling and availability |
| 4. Relationships | nostrsocial | Social graph, trust tiers, guardrails |
| 5. Compass | social-alignment | Value alignment across five lenses |
| Orchestrator | nse-orchestrator | Nervous system connecting all pillars |