vitorpamplona/amethyst

nip85-trusted-assertions

The NIP-85 trusted-assertions model in Quartz (`nip85TrustedAssertions/`) — kind 10040 trust-provider lists, kind 30382 contact cards / user assertions, 30383 event assertions, 30384 addressable assertions, 30385 external-id assertions. Use when building or parsing these events, working with the typed tags (RankTag, HopsTag, FollowerCountTag, ServiceProviderTag/ServiceType, …), wiring a consumer that resolves a 10040 provider entry to the 30382s it signs, ranking on assertion values, or touchin…

First seen Aug 19, 2026

Installation

$ npx skills add vitorpamplona/amethyst --skill nip85-trusted-assertions

Summary

  • The NIP-85 trusted-assertions model in Quartz (`nip85TrustedAssertions/`) — kind 10040 trust-provider lists, kind 30382 contact cards / user assertions, 30383 event assertions, 30384 addressable assertions, 30385 external-id assertions.
  • Use when building or parsing these events, working with the typed tags (RankTag, HopsTag, FollowerCountTag, ServiceProviderTag/ServiceType, …), wiring a consumer that resolves a 10040 provider entry to the 30382s it signs, ranking on assertion values, or touching the GrapeRank publisher, contact-card nicknames, or the trust projection of an external store.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from vitorpamplona/amethyst · top by installs.

npx skills add vitorpamplona/amethyst

Browse all from vitorpamplona/amethyst

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1.6K
License LICENSE
Default branch main
Open issues 111
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,443 B
  • docs SUMMARY.md 631 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 4 installs

SKILL.md

NIP-85 Trusted Assertions — the Quartz model

Package: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/. NIP-85 is still an evolving spec; this package is the operative definition of what Amethyst-family software writes and reads. This skill states the model (who signs what about whom), the exact kind/d-tag/tag vocabulary, and what consumers may — and may not — assume.

The model in one paragraph

An assertion is signed by the asserting party (a trust provider service, or the user themself) about a subject named in the d-tag. All assertion kinds are addressable, so "latest card by provider P about subject S" is just the addressable coordinate (kind, P, S) and supersession is standard NIP-01 latest-wins. Discovery is the observer's kind 10040 list: each entry says "for metric M on kind K, I trust provider P — fetch their assertions at relay R". Quartz enforces none of this cryptographically beyond normal event signatures; the 10040→assertion link is consumer-side convention (see "Authorization" below).

Kind map

Kind Class Kind class d-tag = the subject Content
10040 list/TrustProviderListEvent replaceable (none — always "") NIP-44 private provider entries (optional)
30382 users/ContactCardEvent addressable target user's pubkey (hex) NIP-44 private tags (petname/summary/emoji)
30383 events/EventAssertionEvent addressable target event id (hex) ""
30384 addressables/AddressableAssertionEvent addressable target coordinate kind:pubkey:dtag ""
30385 externalIds/ExternalIdAssertionEvent addressable external identifier (e.g. isbn:978-0-13-468599-1) ""

Addresses: ContactCardEvent.createAddress(owner, target)Address(30382, owner, target) (owner = signer, target = subject). TrustProviderListEvent.createAddress(pubKey) uses FIXEDDTAG = "". AssertionEventTest.eventKindsAreCorrect pins all five numbers.

ContactCardEvent is also a SearchableEvent — it indexes only the public petname/summary tags plus topics; the encrypted card content is intentionally never indexed.

The 10040 provider entry (ServiceProviderTag / ServiceType)

There is no fixed tag name: tag[0] is the service string.

["30382:rank", "<provider pubkey, 64 hex>", "wss://nip85.brainstorm.world"]
  • ServiceType(kind, type) parses/renders "<kind>:<type>" — kind must be an int, the first

: splits, colons in the remainder stay in type. ServiceType.isOfKind is the allocation-free prefix check.

  • ServiceProviderTag.parse requires ≥3 elements, non-empty service, 64-char pubkey

(length-only check), and a normalizable relay URL (RelayUrlNormalizer.normalizeOrNull) — entries failing any check are silently dropped, which is what keeps foreign tags like ["client","nostria"] out (regression-tested in ServiceTypeParserTest).

  • Entries may be public (tag array) or private (NIP-44 content); create/add take

isPrivate. remove always needs decryption and strips from both sides by parsed-value equality.

  • object ProviderTypes (list/tags/ServiceType.kt) enumerates the known service types —

30382:rank, 30382:followers, 30382:firstcreatedat, per-metric 30383:/30384:/ 30385:*, etc. It is an open vocabulary: real 10040s in the wild (see the fiatjaf → brainstorm fixture in commonTest/.../nip85TrustedAssertions/ServiceParser.kt) carry types Quartz doesn't enumerate (30382:personalizedGrapeRank_influence, 30382:hops, 30382:verifiedFollowersCount, …). Parse any kind:type; special-case only what you rank on.

Authorization — what a consumer may assume

  • **A 30382 (or 30383/…) is meaningful to an observer only if its author is listed in the

observer's 10040 for a matching service type. Quartz does not enforce this; the consuming code does. The in-repo pattern is commons/.../model/nip85TrustedAssertions/UserCardsCache.kt: rankFlow(trustProviderList) picks the received card whose author pubkey equals the provider entry's pubkey** and reads rank() from it. Assertions from unlisted signers are simply ignored for trust purposes (they may still be stored; dropping them — as an external store's orphan sweep does — is a legitimate storage policy, not a protocol rule).

  • What an entry authorizes is scoped by its ServiceType: 30382:rank authorizes that

provider's user-rank cards, nothing else. Amethyst models this as one provider slot per metric (liveUserRankProvider, liveUserFollowerCount in amethyst/.../model/trustedAssertions/TrustProviderListState.kt).

  • Multi-provider combination is unprescribed. When two listed providers assert different

ranks, there is no spec'd merge; Amethyst avoids the question by selecting one provider per metric slot. Consumers choose their own policy — document it.

  • The relay URL in the entry is a fetch hint, and it is honored:

amethyst/.../UserCardsSubAssembler.kt subscribes for cards at the provider's declared relay (kinds=[30382], authors=[provider], #d=[targets]).

The dual use of kind 30382

The same kind serves two roles, distinguished by author:

  1. Provider WoT cards — signed by a trust provider; public metric tags (rank,

followers, hops, …); this is what 10040 discovery points at.

  1. The account's own contact cards (nicknames, NIP-81-style) — signed by the account,

one per target user. The petname, summary, and their NIP-30 emoji mappings always live in the NIP-44 encrypted content, never in public tags (ContactCardEvent.build/ updatePetNameAndSummary strip stray public copies; asserted by ContactCardPetNameTest). commons/.../ContactCardsState.kt keys everything on author == account and ignores provider cards.

Tag vocabulary and value semantics

All tag classes share one shape: TAG_NAME + parse(tag) (null on wrong name/empty/non-numeric value — a bad tag is dropped, never an error) + assemble(value)[name, value.toString()]. A missing tag means "unknown" (null accessor), never zero. There is deliberately no range validation (rank isn't clamped, hours aren't checked against 0–23, counts may be negative) — consumers must defend.

On 30382 (users/tags/, accessors on ContactCardEvent and as TagArray extensions in users/TagArrayExt.kt so they also work on decrypted private arrays):

Tag name Accessor Type Semantics
rank rank() Int Provider-relative score; higher is better. GrapeRank publishes round(score × 100) (so 0–100 in practice), but nothing enforces a scale — treat it as comparable only within one provider.
followers followerCount() Int Follower count as the provider computes it (cumulative, provider-defined).
hops hops() Int Shortest follow-path length from the observer the provider computed for to the subject (1 = directly followed). Mirrors Brainstorm GrapeRank's hops. The only tag with KDoc.
firstcreatedat firstCreatedAt() Long Unix seconds of subject's earliest known event.
postcnt / replycnt / reactions_cnt postCount() etc. Int Activity counts.
zapamtrecd / zapamtsent zapAmountReceived()/…Sent() Long Sats.
zapcntrecd / zapcntsent zapCountReceived()/…Sent() Int Counts.
zapavgamtdayrecd / zapavgamtdaysent zapAvgAmountDay…() Long Sats/day averages.
reportscntrecd / reportscntsent reportsCount…() Int NIP-56 report counts.
t (repeatable) topics() List\<String> Subject's topics/interests.
activehoursstart / activehoursend activeHours…() Int Hour-of-day; no timezone is specified in code — treat as provider-defined (UTC in practice) and unclamped.
petname / summary petName()/summary() String Nickname fields — conventionally private (see dual use above).

On 30383/30384 (tags/, shared): rank, commentcnt, quotecnt, repostcnt, reactioncnt, zapcnt (Int) and zapamount (Long, sats). On 30385: only rank, commentcnt, reactioncnt.

Building and parsing (use the typed helpers, not raw arrayOf)

// Provider list: declare a rank provider (this is what `amy graperank register` does)
val tag = ServiceProviderTag(ProviderTypes.rank, providerPubkeyHex, relayUrl)
val list = TrustProviderListEvent.create(tag, isPrivate = false, signer)
// or append to an existing one:
val updated = TrustProviderListEvent.add(existing, tag, isPrivate = false, signer)
val providers: List<ServiceProviderTag> = updated.serviceProviders()          // public
val private = updated.privateTags(signer)?.serviceProviders()                 // private side

// Provider-style contact card (public metrics) — the GrapeRankPublisher pattern:
val card = ContactCardEvent.create(
    targetUser = subjectPubkey,
    signer = providerSigner,
    publicInitializer = {
        rank(87)
        followers(1234)
        hops(2)
    },
)
card.aboutUser()   // d-tag → subject pubkey
card.rank()        // 87

// Event assertion: unsigned template only (30383/84/85 have build(), no create())
val template = EventAssertionEvent.build(targetEventId) {
    rank(12)
    reactionCount(40)
    zapAmount(2100)
}
val signed = signer.sign(template)

Worked end-to-end example

Observer O trusts provider P for user ranks (kind 10040, replaceable, by O):

{ "kind": 10040, "pubkey": "<O>",
  "tags": [
    ["30382:rank",      "<P>", "wss://nip85.brainstorm.world"],
    ["30382:followers", "<P>", "wss://nip85.brainstorm.world"]
  ],
  "content": "" }

Provider P asserts about subject S (kind 30382, addressable at 30382:<P>:<S>):

{ "kind": 30382, "pubkey": "<P>",
  "tags": [
    ["d", "<S>"],
    ["rank", "87"], ["followers", "1234"], ["hops", "2"]
  ],
  "content": "" }

P asserts about an event E (kind 30383, addressable at 30383:<P>:<E>):

{ "kind": 30383, "pubkey": "<P>",
  "tags": [["d", "<E>"], ["rank", "12"], ["reaction_cnt", "40"], ["zap_amount", "2100"]],
  "content": "" }

Consumption chain: read O's 10040 → entry matching ServiceType(30382, "rank") → subscribe {kinds:[30382], authors:["<P>"], "#d":["<S>", …]} at the hinted relay → newest card per address wins → rank().

Literal fixtures: quartz/src/commonTest/.../nip85TrustedAssertions/ServiceParser.kt (a real 10040 — fiatjaf's, pointing at the Brainstorm provider) and AssertionEventTest.kt (all four assertion kinds with every tag populated).

Freshness / supersession

Assertions are addressable: latest per (kind, author, d-tag) wins; there is no expiry tag convention and no prescribed refresh cadence — staleness policy is the consumer's. Writers should avoid churn: GrapeRankPublisher re-signs a card only when (rank, followers, hops) actually changed, and retracts with a NIP-09 kind-5 carrying the card's a-tag (30382:<provider>:<target>).

Stability notes (as of 2026-08)

  • Settled (shipped consumers on both ends): the kind map; ServiceProviderTag entry shape;

rank/followers/hops on 30382; petname/summary-in-encrypted-content; 10040 relay-hint consumption.

  • Written but lightly consumed (parse, but gate ranking features carefully): the activity/

zap/report count tags, activehours* (no timezone semantics), 30383/30384/30385 (builders + tests exist; no in-repo publisher yet).

  • Known warts: ServiceProviderTag.assemble(id: ServiceProviderTag) infers Array<Any>

dead code, don't use it; SummaryTag.assemble(ip:)/ActiveHours*Tag.assemble(count:) params are misnamed; the tests live under commonTest/.../experimental/nip85TrustedAssertions/ (stale path); TrustProviderListEvent extends the addressable base, so a stray on-wire d tag is reflected by dTag() even though the convention is "".

Where it's consumed (reading list)

  • Publisher: quartz/.../experimental/graperank/GrapeRankPublisher.kt (canonical 30382

writer), cli/.../graperank/ (amy graperank register|unregister|providers|publish).

  • Client model: commons/.../model/nip85TrustedAssertions/ (ContactCardsState,

UserCardsCache, ContactCardDecryptionCache, TrustProviderListDecryptionCache), amethyst/.../model/trustedAssertions/TrustProviderListState.kt.

  • Relay plumbing: commons/.../relayClient/assemblers/ContactCardFilters.kt,

amethyst/.../reqCommand/user/watchers/UserCardsSubAssembler.kt.