diff --git a/doc/nostr/nip-place-reviews.md b/doc/nostr/nip-place-reviews.md new file mode 100644 index 0000000..a0221d2 --- /dev/null +++ b/doc/nostr/nip-place-reviews.md @@ -0,0 +1,343 @@ +# NIP-XX: Place Reviews + +## Abstract + +This NIP defines a standardized event format for decentralized place reviews using Nostr. Reviews are tied to real-world locations (e.g. OpenStreetMap POIs) via tags, and include structured, multi-aspect ratings, a binary recommendation signal, and optional contextual metadata. + +The design prioritizes: + +* Small event size +* Interoperability across clients +* Flexibility for different place types +* Efficient geospatial querying using geohashes + +--- + +## Event Kind + +`kind: 30315` (suggested; subject to coordination) + +--- + +## Tags + +Additional tags MAY be included by clients but are not defined by this specification. + +This NIP reuses and builds upon existing Nostr tag conventions: + +* `i` tag: see NIP-73 (External Content Identifiers) +* `g` tag: geohash-based geotagging (community conventions) + +Where conflicts arise, this NIP specifies the behavior for review events. + +### Required + +#### `i` — Entity Identifier + +Identifies the reviewed place using an external identifier. OpenStreetMap data is the default: + +``` +["i", "osm::"] +``` + +Requirements: + +* For OSM POIs, `` MUST be one of: `node`, `way`, `relation` + +Examples: + +``` +["i", "osm:node:123456"] +["i", "osm:way:987654"] +``` + +--- + +### Geospatial Tags + +#### `g` — Geohash + +Geohash tags are used for spatial indexing and discovery. + +##### Requirements + +* Clients MUST include at least one high-precision geohash (length ≥ 9) + +##### Recommendations + +Clients SHOULD include geohashes at the following resolutions: + +* length 4 — coarse (city-scale discovery) +* length 6 — medium (default query level, ~1 km) +* length 7 — fine (neighborhood, ~150 m) + +Example: + +``` +["g", "thrr"] +["g", "thrrn5"] +["g", "thrrn5k"] +["g", "thrrn5kxyz"] +``` + +##### Querying + +Geospatial queries are performed using the `g` tag. + +* Clients SHOULD query using a single geohash precision level per request +* Clients MAY include multiple geohash values in a filter to cover a bounding box +* Clients SHOULD limit the number of geohash values per query (e.g. ≤ 30) +* Clients MAY reduce precision or split queries when necessary + +Note: Other queries (e.g. fetching reviews for a specific place) are performed using the `i` tag and are outside the scope of geospatial querying. + +--- + +## Content (JSON) + +The event `content` MUST be valid JSON matching the following schema. + +### Schema + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["version", "ratings"], + "additionalProperties": false, + "properties": { + "version": { + "type": "integer", + "const": 1 + }, + "ratings": { + "type": "object", + "required": ["quality"], + "additionalProperties": false, + "properties": { + "quality": { "$ref": "#/$defs/score" }, + "value": { "$ref": "#/$defs/score" }, + "experience": { "$ref": "#/$defs/score" }, + "accessibility": { "$ref": "#/$defs/score" }, + "aspects": { + "type": "object", + "minProperties": 1, + "maxProperties": 20, + "additionalProperties": { "$ref": "#/$defs/score" }, + "propertyNames": { + "pattern": "^[a-z][a-z0-9_]{1,31}$" + } + } + } + }, + "recommend": { + "type": "boolean" + }, + "familiarity": { + "type": "string", + "enum": ["low", "medium", "high"], + "description": "User familiarity: low = first visit; medium = occasional; high = frequent" + }, + "context": { + "type": "object", + "additionalProperties": false, + "properties": { + "visited_at": { + "type": "integer", + "minimum": 0 + }, + "duration_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 1440 + }, + "party_size": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + "review": { + "type": "object", + "additionalProperties": false, + "properties": { + "text": { + "type": "string", + "maxLength": 1000 + }, + "language": { + "type": "string", + "pattern": "^[a-z]{2}(-[A-Z]{2})?$" + } + } + } + }, + "$defs": { + "score": { + "type": "integer", + "minimum": 1, + "maximum": 10 + } + } +} +``` + +--- + +## Example + +### Restaurant Review Event + +#### Tags + +``` +[ + ["i", "osm:node:123456"], + ["g", "thrr"], + ["g", "thrrn5"], + ["g", "thrrn5k"], + ["g", "thrrn5kxyz"] +] +``` + +#### Content + +```json +{ + "version": 1, + "ratings": { + "quality": 9, + "value": 8, + "experience": 9, + "accessibility": 7, + "aspects": { + "food": 9, + "service": 6, + "ambience": 8, + "wait_time": 5 + } + }, + "recommend": true, + "familiarity": "medium", + "context": { + "visited_at": 1713200000, + "duration_minutes": 90, + "party_size": 2 + }, + "review": { + "text": "Excellent food with bold flavors. Service was a bit slow, but the atmosphere made up for it.", + "language": "en" + } +} +``` + +--- + +## Semantics + +### Ratings + +* Scores are integers from 1 to 10 +* `quality` is required and represents the core evaluation of the place +* Other fields are optional and context-dependent + +### Aspects + +* Free-form keys allow domain-specific ratings +* Clients MAY define and interpret aspect keys +* Clients SHOULD reuse commonly established aspect keys where possible + +--- + +## Recommendation Signal + +The `recommend` field represents a binary verdict: + +* `true` → user recommends the place +* `false` → user does not recommend the place + +Clients SHOULD strongly encourage users to provide this value. + +--- + +## Familiarity + +Represents user familiarity with the place: + +* `low` → first visit or limited exposure +* `medium` → occasional visits +* `high` → frequent or expert-level familiarity + +Clients MAY use this signal for weighting during aggregation. + +--- + +## Context + +Optional metadata about the visit. + +* `visited_at` is a Unix timestamp +* `duration_minutes` represents time spent +* `party_size` indicates group size + +--- + +## Interoperability + +This specification defines a content payload only. + +* In Nostr: place identity is conveyed via tags +* In other protocols (e.g. ActivityPub, AT Protocol): identity MUST be mapped to the equivalent field (e.g. `object`) + +Content payloads SHOULD NOT include place identifiers. + +--- + +## Rationale + +### No Place Field in Content + +Avoids duplication and inconsistency with tags. + +### Multi-Aspect Ratings + +Separates concerns (e.g. quality vs service), improving signal quality. + +### Recommendation vs Score + +Binary recommendation avoids averaging pitfalls and improves ranking. + +### Familiarity + +Provides a human-friendly proxy for confidence without requiring numeric input. + +### Geohash Strategy + +Multiple resolutions balance: + +* efficient querying +* small event size +* early-stage discoverability + +--- + +## Future Work + +* Standardized aspect vocabularies +* Reputation and weighting models +* Indexing/aggregation services +* Cross-protocol mappings + +--- + +## Security Considerations + +* Clients SHOULD validate all input +* Malicious or spam reviews may require external moderation or reputation systems + +--- + +## Copyright + +This NIP is public domain. diff --git a/doc/nostr/notes.md b/doc/nostr/notes.md new file mode 100644 index 0000000..8aec90a --- /dev/null +++ b/doc/nostr/notes.md @@ -0,0 +1,6 @@ +# Notes + +- NIP-73 for external IDs ("osm:node:123456"): https://github.com/nostr-protocol/nips/blob/744bce8fcae0aca07b936b6662db635c8b4253dd/73.md +- Places NIP-XX draft PR: https://github.com/nostr-protocol/nips/pull/927 +- NPM package for generating multi-resolution geotags: https://sandwichfarm.github.io/nostr-geotags/#md:nostr-geotags +- AppleSauce docs for AI agents: https://applesauce.build/introduction/mcp-server.html diff --git a/doc/nostr/ranking.md b/doc/nostr/ranking.md new file mode 100644 index 0000000..7f5489e --- /dev/null +++ b/doc/nostr/ranking.md @@ -0,0 +1,251 @@ +# Ranking Algorithm + +Your inputs: + +* many users +* partial ratings +* different priorities + +Your output: + +> “Best place *for this user right now*” + +--- + +## Step 1: Normalize scores + +Convert 1–10 → 0–1: + +```text +normalized_score = (score - 1) / 9 +``` + +Why: + +* easier math +* comparable across aspects + +--- + +## Step 2: Per-aspect aggregation (avoid averages trap) + +Instead of mean, compute: + +### A. Positive ratio + +```text +positive = score >= 7 +negative = score <= 4 +``` + +Then: + +```text +positive_ratio = positive_votes / total_votes +``` + +--- + +### B. Confidence-weighted score + +Use something like a **Wilson score interval** (this is key): + +* prevents small-sample abuse +* avoids “1 review = #1 place” + +--- + +## Step 3: Build aspect scores + +For each aspect: + +```text +aspect_score = f( + positive_ratio, + confidence, + number_of_reviews +) +``` + +You can approximate with: + +```text +aspect_score = positive_ratio * log(1 + review_count) +``` + +(Simple, works surprisingly well) + +--- + +## Step 4: User preference weighting + +User defines: + +```json +{ + "quality": 0.5, + "value": 0.2, + "service": 0.2, + "speed": 0.1 +} +``` + +Then: + +```text +final_score = Σ (aspect_score × weight) +``` + +--- + +## Step 5: Context filtering (this is your unfair advantage) + +Filter reviews before scoring: + +* time-based: + + * “last 6 months” +* context-based: + + * lunch vs dinner + * solo vs group + +This is something centralized platforms barely do. + +--- + +## Step 6: Reviewer weighting (later, but powerful) + +Weight reviews by: + +* consistency +* similarity to user preferences +* past agreement + +This gives you: + +> “people like you liked this” + +--- + +# 3. Example end-to-end + +### Raw reviews: + +| User | Food | Service | +| ---- | ---- | ------- | +| A | 9 | 4 | +| B | 8 | 5 | +| C | 10 | 3 | + +--- + +### Derived: + +* food → high positive ratio (~100%) +* service → low (~33%) + +--- + +### User preferences: + +```json +{ + "food": 0.8, + "service": 0.2 +} +``` + +→ ranks high + +Another user: + +```json +{ + "food": 0.3, + "service": 0.7 +} +``` + +→ ranks low + +👉 Same data, different truth +That’s your killer feature. + +--- + +# 4. Critical design choices (don’t skip these) + +## A. No global score in protocol + +Let clients compute it. + +--- + +## B. Embrace incomplete data + +Most reviews will have: + +* 1–3 aspects only + +That’s fine. + +--- + +## C. Time decay (important) + +Recent reviews should matter more: + +```text +weight = e^(-λ × age) +``` + +--- + +## D. Anti-gaming baseline + +Even in nostr: + +* spam will happen + +Mitigation later: + +* require minimum interactions +* reputation layers + +--- + +# 5. What you’ve built (zooming out) + +This is not a review system. + +It’s: + +> A decentralized, multi-dimensional reputation graph for real-world places + +That’s much bigger. + +--- + +# 6. Next step (if you want to go deeper) + +We can design: + +### A. Query layer + +* how clients fetch & merge nostr reviews efficiently + +### B. Anti-spam / trust model + +* web-of-trust +* staking / reputation + +### C. OSM integration details + +* handling duplicates +* POI identity conflicts + +--- + +If I had to pick one next: +👉 **trust/reputation system** — because without it, everything you built *will* get gamed. diff --git a/doc/nostr/ratings.md b/doc/nostr/ratings.md new file mode 100644 index 0000000..223b7f6 --- /dev/null +++ b/doc/nostr/ratings.md @@ -0,0 +1,110 @@ +# Canonical Aspect Vocabulary (v0.1) + +You want a **soft standard**, not a rigid schema. + +Think: + +* “recommended keys” (clients SHOULD use) +* not “required keys” (protocol enforces) + +--- + +## A. Core universal aspects (keep this small) + +These should work for *any* place: + +```json +[ + "quality", // core offering (food, repair, exhibits, etc.) + "value", // value for money/time + "experience", // comfort, usability, vibe + "accessibility" // ease of access, inclusivity +] +``` + +### Why these work + +* **quality** → your “product” abstraction (critical) +* **value** → universally meaningful signal +* **experience** → captures everything “soft” +* **accessibility** → often ignored but high utility + +👉 Resist adding more. Every extra “universal” weakens the concept. + +--- + +## B. Common cross-domain aspects (recommended pool) + +Not universal, but widely reusable: + +```json +[ + "service", // human interaction + "speed", // waiting time / turnaround + "cleanliness", + "safety", + "reliability", + "atmosphere" +] +``` + +These apply to: + +* restaurants, garages, clinics, parks, etc. + +--- + +## C. Domain-specific examples (NOT standardized) + +Let clients define freely: + +```json +{ + "restaurant": ["food", "drinks"], + "bar": ["drinks", "music"], + "garage": ["work_quality", "honesty"], + "park": ["greenery", "amenities"], + "museum": ["exhibits", "crowding"] +} +``` + +--- + +## D. Key rule (this prevents chaos) + +👉 **Aspect keys MUST be lowercase snake_case** + +👉 **Meaning is defined socially, not technically** + +To reduce fragmentation: + +* publish a **public registry (GitHub repo)** +* clients can: + + * suggest additions + * map synonyms + +--- + +## E. Optional normalization hint (important later) + +Allow this: + +```json +"aspect_aliases": { + "food": "quality", + "work_quality": "quality" +} +``` + +Not required, but useful for aggregation engines. + +--- + +## Notes + +Map familiarity in UI to: + +* high: “I know this place well” +* medium: “Been a few times” +* low: “First visit” diff --git a/doc/nostr/review-schema.json b/doc/nostr/review-schema.json new file mode 100644 index 0000000..b2a2cb0 --- /dev/null +++ b/doc/nostr/review-schema.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.org/schemas/place-review.json", + "title": "Decentralized Place Review (Nostr/Event Content)", + "type": "object", + "required": ["version", "place", "ratings"], + "additionalProperties": false, + + "properties": { + "version": { + "type": "integer", + "const": 1 + }, + + "ratings": { + "type": "object", + "required": ["quality"], + "additionalProperties": false, + "properties": { + "quality": { "$ref": "#/$defs/score" }, + "value": { "$ref": "#/$defs/score" }, + "experience": { "$ref": "#/$defs/score" }, + "accessibility": { "$ref": "#/$defs/score" }, + + "aspects": { + "type": "object", + "minProperties": 1, + "maxProperties": 20, + "additionalProperties": { "$ref": "#/$defs/score" }, + "propertyNames": { + "pattern": "^[a-z][a-z0-9_]{1,31}$" + } + } + } + }, + + "recommend": { + "type": "boolean", + "description": "Whether the user recommends this place to others" + }, + + "familiarity": { + "type": "string", + "enum": ["low", "medium", "high"], + "description": "User familiarity with the place. Suggested interpretation: 'low' = first visit or very limited experience; 'medium' = visited a few times or moderate familiarity; 'high' = frequent visitor or strong familiarity." + }, + + "context": { + "type": "object", + "additionalProperties": false, + "properties": { + "visited_at": { + "type": "integer", + "minimum": 0 + }, + "duration_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 1440 + }, + "party_size": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + + "review": { + "type": "object", + "additionalProperties": false, + "properties": { + "text": { + "type": "string", + "maxLength": 1000 + }, + "language": { + "type": "string", + "pattern": "^[a-z]{2}(-[A-Z]{2})?$" + } + } + } + }, + + "$defs": { + "score": { + "type": "integer", + "minimum": 1, + "maximum": 10 + } + } +}