New Photographers category (blue-indigo) — separate from Filmers: - New addressable kind 35004 + SkatePhotographer type + parseSkatePhotographer. - Rich photographer taxonomy as queryable `t` tags: services (action, sequence, editorial, product, events…), camera bodies, lenses, lighting (Profoto/Elinchrom/Godox/triggers…), support/rig, and shooting style, plus a freeform "gear" tag and a portfolio link. - BecomePhotographerPage form with chip-grid selectors for all gear categories + "what's in the bag" textarea, wired to /become-photographer. - The Lab: 4th "Photo" tab (indigo accent) with its own filters, cards, empty state; OfferFAB gains "Offer Photography"; useLabProfiles fetches 35004; CoachDetailPage renders photographers (and editors) properly with per-type accent, gear + portfolio display. - NIP.md: documented the whole Lab (kinds 35001-35004) incl. photographer. Filmer improvements (now video-focused, photography split out): - Removed the token "Action Photography" service; added follow/line filming. - Richer equipment list: cinema camera, HVX, action cam, gimbal, 4K/HFR. - Filmers gain a freeform "what's in the bag" gear field + a reel/portfolio link, both surfaced on the detail page. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1512 lines
45 KiB
Markdown
1512 lines
45 KiB
Markdown
# SkateSpots Protocol
|
||
|
||
This document defines the Nostr event kinds used by the SkateSpots application for managing skateboarding locations, shops, and related content.
|
||
|
||
## Skate Shops (kind 30017)
|
||
|
||
Skate shops use the NIP-15 marketplace "stall" kind (30017) extended with skateboarding-specific tags. This enables interoperability with existing Nostr marketplace clients.
|
||
|
||
### Event Format
|
||
|
||
```json
|
||
{
|
||
"kind": 30017,
|
||
"content": "<shop description>",
|
||
"tags": [
|
||
["d", "<unique-identifier>"],
|
||
["name", "<shop name>"],
|
||
["g", "<geohash>"],
|
||
["location", "<shop address>"],
|
||
["t", "skateshop"],
|
||
["phone", "<phone number>"],
|
||
["email", "<email address>"],
|
||
["website", "<website URL>"],
|
||
["instagram", "<instagram handle>"],
|
||
["hours", "<hours of operation>"],
|
||
["imeta", "url <image-url>", "m image/jpeg"],
|
||
["alt", "Skate shop: <name>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Required Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `d` | Unique identifier for the shop |
|
||
| `name` | Shop name |
|
||
| `g` | Geohash encoding the shop's coordinates |
|
||
| `t` | Must include `skateshop` to identify as a skate shop |
|
||
| `location` | Physical shop address |
|
||
|
||
### Optional Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `phone` | Contact phone number |
|
||
| `email` | Contact email address |
|
||
| `website` | Shop website URL |
|
||
| `instagram` | Instagram handle |
|
||
| `hours` | Hours of operation (freeform text) |
|
||
| `imeta` | Image metadata per NIP-92 |
|
||
| `lat` | Latitude (decimal) |
|
||
| `lon` | Longitude (decimal) |
|
||
|
||
### Shop Ownership Verification
|
||
|
||
Shops can be verified using **NIP-05** domain verification. A shop is considered verified when:
|
||
|
||
1. The shop has a `website` tag with a domain (e.g., `supremenewyork.com`)
|
||
2. The shop owner's NIP-05 identifier matches the shop's domain (e.g., `owner@supremenewyork.com`)
|
||
|
||
This proves the shop owner controls the domain, establishing ownership.
|
||
|
||
### Example
|
||
|
||
```json
|
||
{
|
||
"kind": 30017,
|
||
"pubkey": "abc123...",
|
||
"created_at": 1703001600,
|
||
"content": "Los Angeles' premier skateboard shop since 1994. Full selection of decks, trucks, wheels, and streetwear.",
|
||
"tags": [
|
||
["d", "supreme-la"],
|
||
["name", "Supreme Los Angeles"],
|
||
["g", "9q5ctr"],
|
||
["location", "411 N Fairfax Ave, Los Angeles, CA 90036"],
|
||
["t", "skateshop"],
|
||
["phone", "+1-323-655-6205"],
|
||
["website", "https://www.supremenewyork.com"],
|
||
["instagram", "@supremenewyork"],
|
||
["hours", "Mon-Sat 11AM-7PM, Sun 12PM-6PM"],
|
||
["imeta", "url https://example.com/shop-front.jpg", "m image/jpeg"],
|
||
["lat", "34.0737"],
|
||
["lon", "-118.3614"],
|
||
["alt", "Skate shop: Supreme Los Angeles"]
|
||
],
|
||
"id": "...",
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
### Querying Shops
|
||
|
||
```json
|
||
{
|
||
"kinds": [30017],
|
||
"#t": ["skateshop"]
|
||
}
|
||
```
|
||
|
||
## Skate Spot (kind 30818)
|
||
|
||
A skate spot is an addressable event that represents a skateboarding location anywhere in the world. Users can create, share, and discover skate spots.
|
||
|
||
### Event Format
|
||
|
||
```json
|
||
{
|
||
"kind": 30818,
|
||
"content": "<detailed description of the skate spot>",
|
||
"tags": [
|
||
["d", "<unique-identifier>"],
|
||
["title", "<name of the skate spot>"],
|
||
["g", "<geohash>"],
|
||
["location", "<human-readable address>"],
|
||
["category", "<spot-category>"],
|
||
["t", "skatespot"],
|
||
["t", "<feature-tag>"],
|
||
["t", "<surface-tag>"],
|
||
["t", "<vibe-tag>"],
|
||
["imeta", "url <image-url>", "m image/jpeg"],
|
||
["alt", "Skate spot: <title>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Required Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `d` | Unique identifier for the spot (UUID or slug) |
|
||
| `title` | Human-readable name of the skate spot |
|
||
| `g` | Geohash encoding the spot's geographic coordinates (minimum 6 characters for ~1km precision) |
|
||
| `category` | Primary spot category (see Spot Categories below) |
|
||
| `t` | Must include `skatespot` for discoverability |
|
||
| `alt` | Human-readable description for clients that don't understand this kind (NIP-31) |
|
||
|
||
### Optional Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `location` | Human-readable address or location description |
|
||
| `imeta` | Image metadata per NIP-92 |
|
||
| `lat` | Latitude (decimal) |
|
||
| `lon` | Longitude (decimal) |
|
||
| `access` | Access type: `public`, `private`, `locals` |
|
||
| `contact` | Contact info for the spot |
|
||
| `t` | Freeform hashtags for features, surfaces, vibes (max 20) |
|
||
|
||
### Spot Categories
|
||
|
||
The `category` tag defines the primary type of spot. Only ONE category per spot:
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `street` | Street spots (stairs, rails, ledges, etc.) |
|
||
| `park` | Skateparks (public or private) |
|
||
| `diy` | DIY/homemade spots |
|
||
| `plaza` | Public plazas with skate-friendly architecture |
|
||
|
||
### Freeform Tags (`t` tags)
|
||
|
||
In addition to the required `skatespot` tag, spots can have up to **20 freeform tags** for detailed categorization. Tags are stored lowercase without `#` prefix.
|
||
|
||
#### Feature Tags (obstacles/terrain)
|
||
`ledge`, `rail`, `handrail`, `flatrail`, `kinked-rail`, `rainbow-rail`, `stairs`, `gap`, `bank`, `quarter`, `halfpipe`, `bowl`, `pool`, `miniramp`, `vert`, `hubba`, `manual-pad`, `flatground`, `curb`, `bump`, `pyramid`, `euro-gap`, `kicker`, `hip`, `spine`, `cradle`, `snake-run`, `pump-track`, `wallride`, `drop`, `loading-dock`
|
||
|
||
#### Surface Tags (materials)
|
||
`concrete`, `marble`, `granite`, `brick`, `metal`, `wood`, `asphalt`, `tile`, `smooth`, `rough`, `crusty`, `slippery`, `painted`
|
||
|
||
#### Size Tags
|
||
`small`, `medium`, `large`, `massive`, `mellow`, `steep`, `low`, `high`, `long`, `short`
|
||
|
||
#### Stair Counts
|
||
`2-stair`, `3-stair`, `4-stair`, `5-stair`, `6-stair`, `7-stair`, `8-stair`, `9-stair`, `10-stair`, `10-plus-stair`
|
||
|
||
#### Vibe/Condition Tags
|
||
`chill`, `famous`, `classic`, `hidden-gem`, `locals-only`, `beginner-friendly`, `intermediate`, `advanced`, `pro-spot`, `night-spot`, `covered`, `lit`, `shaded`, `security`, `bust-factor`, `skate-stopped`, `well-maintained`, `run-down`
|
||
|
||
#### Location Context Tags
|
||
`downtown`, `industrial`, `residential`, `campus`, `waterfront`, `rooftop`, `underground`, `parking-garage`, `school`, `business-district`
|
||
|
||
### Tag Normalization Rules
|
||
|
||
1. **Lowercase**: All tags stored in lowercase (`Ledge` → `ledge`)
|
||
2. **No spaces**: Use hyphens for multi-word tags (`night spot` → `night-spot`)
|
||
3. **No special characters**: Alphanumeric and hyphens only
|
||
4. **No `#` prefix**: Tags stored without hash (`ledge` not `#ledge`)
|
||
5. **Max 20 tags**: Prevent spam/over-tagging
|
||
|
||
### Example
|
||
|
||
```json
|
||
{
|
||
"kind": 30818,
|
||
"pubkey": "abc123...",
|
||
"created_at": 1703001600,
|
||
"content": "Classic LA spot with a perfect 10-stair and hubba ledge. Usually busy on weekends. Security is chill during the day but kicks out after 6pm.",
|
||
"tags": [
|
||
["d", "hollywood-high"],
|
||
["title", "Hollywood High School"],
|
||
["g", "9q5ctr"],
|
||
["location", "1521 N Highland Ave, Los Angeles, CA"],
|
||
["category", "street"],
|
||
["access", "public"],
|
||
["t", "skatespot"],
|
||
["t", "stairs"],
|
||
["t", "10-stair"],
|
||
["t", "hubba"],
|
||
["t", "ledge"],
|
||
["t", "marble"],
|
||
["t", "famous"],
|
||
["t", "classic"],
|
||
["t", "pro-spot"],
|
||
["t", "security"],
|
||
["imeta", "url https://example.com/hollywood-high.jpg", "m image/jpeg"],
|
||
["lat", "34.1017"],
|
||
["lon", "-118.3385"],
|
||
["alt", "Skate spot: Hollywood High School"]
|
||
],
|
||
"id": "...",
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
## Querying Skate Spots
|
||
|
||
### All Spots
|
||
```json
|
||
{
|
||
"kinds": [30818],
|
||
"#t": ["skatespot"]
|
||
}
|
||
```
|
||
|
||
### Spots by Feature Tag
|
||
```json
|
||
{
|
||
"kinds": [30818],
|
||
"#t": ["skatespot", "ledge"]
|
||
}
|
||
```
|
||
|
||
### Spots by Multiple Tags
|
||
```json
|
||
{
|
||
"kinds": [30818],
|
||
"#t": ["skatespot", "marble", "ledge"]
|
||
}
|
||
```
|
||
|
||
### Spots by Geohash Prefix (Area Search)
|
||
```json
|
||
{
|
||
"kinds": [30818],
|
||
"#g": ["9q5"]
|
||
}
|
||
```
|
||
|
||
Note: Relays may or may not support geohash prefix queries. Clients should fall back to client-side filtering if needed.
|
||
|
||
### Client-Side Category Filtering
|
||
|
||
Since `category` is not a single-letter tag, filter by category client-side after fetching spots:
|
||
|
||
```typescript
|
||
const streetSpots = spots.filter(spot =>
|
||
spot.tags.find(([k, v]) => k === 'category' && v === 'street')
|
||
);
|
||
```
|
||
|
||
## Skate Events
|
||
|
||
Skateboarding events use the standard NIP-52 time-based calendar event (kind 31923) with the hashtag `#skateboarding`:
|
||
|
||
```json
|
||
{
|
||
"kind": 31923,
|
||
"content": "<event description>",
|
||
"tags": [
|
||
["d", "<unique-id>"],
|
||
["title", "<event name>"],
|
||
["start", "<unix-timestamp>"],
|
||
["end", "<unix-timestamp>"],
|
||
["g", "<geohash>"],
|
||
["location", "<venue address>"],
|
||
["t", "skateboarding"],
|
||
["t", "<event-type>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Event Types
|
||
|
||
Common `t` tags for skate events:
|
||
- `contest` - Skateboarding competitions
|
||
- `demo` - Pro demos
|
||
- `session` - Group sessions
|
||
- `workshop` - Lessons or workshops
|
||
- `premiere` - Video premieres
|
||
- `tradeshow` - Industry events
|
||
- `gameskate` - Game of S.K.A.T.E. competitions
|
||
|
||
#### Music & Culture Events
|
||
|
||
Skateboarding and music culture are deeply connected. These event types support concerts, jams, and cultural gatherings:
|
||
- `concert` - Live music performances at skate venues
|
||
- `jam` - Skate jams with music, art, and community vibes
|
||
- `festival` - Multi-day skateboarding festivals (e.g., Tampa Pro, Dew Tour)
|
||
- `popup` - Pop-up events, brand activations, and temporary installations
|
||
|
||
Events can have multiple type tags. For example, a video premiere with a live band would have both `premiere` and `concert` tags.
|
||
|
||
## Social Interaction
|
||
|
||
Comments and reactions to skate spots and events use standard Nostr kinds:
|
||
|
||
- **Reactions** (kind 7): React to spots or events with emoji
|
||
- **Comments** (kind 1111): Comment on spots or events per NIP-22
|
||
- **Zaps** (kind 9735): Send sats to spot creators or event organizers
|
||
|
||
## Spot Collections (kind 30003)
|
||
|
||
Spot collections use NIP-51 "Bookmark sets" (kind 30003) to allow users to organize skate spots into personalized lists. This enables interoperability with other Nostr bookmark clients.
|
||
|
||
### Event Format
|
||
|
||
```json
|
||
{
|
||
"kind": 30003,
|
||
"content": "<collection description>",
|
||
"tags": [
|
||
["d", "<unique-identifier>"],
|
||
["title", "<collection name>"],
|
||
["t", "skatespots"],
|
||
["description", "<collection description>"],
|
||
["image", "<cover image URL>"],
|
||
["a", "30818:<pubkey>:<spot-d-tag>"],
|
||
["a", "30818:<pubkey>:<spot-d-tag>"],
|
||
["alt", "Spot collection: <title>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Required Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `d` | Unique identifier for the collection |
|
||
| `title` | Collection name |
|
||
| `t` | Must include `skatespots` to identify as a spot collection |
|
||
|
||
### Optional Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `description` | Description of the collection |
|
||
| `image` | Cover image URL |
|
||
| `a` | Reference to a skate spot (format: `30818:pubkey:d-tag`) |
|
||
| `alt` | Human-readable description for clients that don't understand this kind |
|
||
|
||
### Example
|
||
|
||
```json
|
||
{
|
||
"kind": 30003,
|
||
"pubkey": "abc123...",
|
||
"created_at": 1703001600,
|
||
"content": "My favorite spots from my Barcelona trip in summer 2024",
|
||
"tags": [
|
||
["d", "barcelona-trip-2024-1703001600"],
|
||
["title", "Barcelona Trip 2024"],
|
||
["t", "skatespots"],
|
||
["description", "My favorite spots from my Barcelona trip in summer 2024"],
|
||
["image", "https://example.com/barcelona-cover.jpg"],
|
||
["a", "30818:abc123:macba"],
|
||
["a", "30818:def456:parallel-plaza"],
|
||
["a", "30818:ghi789:sants-station"],
|
||
["alt", "Spot collection: Barcelona Trip 2024"]
|
||
],
|
||
"id": "...",
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
### Querying Collections
|
||
|
||
```json
|
||
{
|
||
"kinds": [30003],
|
||
"authors": ["<user-pubkey>"],
|
||
"#t": ["skatespots"]
|
||
}
|
||
```
|
||
|
||
## Gear Listings (kind 30402)
|
||
|
||
Gear listings use the NIP-99 Classified Listings format (kind 30402) for peer-to-peer skateboarding gear sales. This enables skaters to buy and sell used equipment directly at skate spots.
|
||
|
||
### Event Format
|
||
|
||
```json
|
||
{
|
||
"kind": 30402,
|
||
"content": "<detailed description>",
|
||
"tags": [
|
||
["d", "<unique-identifier>"],
|
||
["title", "<item name>"],
|
||
["t", "skateboarding"],
|
||
["t", "<category>"],
|
||
["price", "<amount>", "<currency>"],
|
||
["price_type", "<fixed|free|trade|offer>"],
|
||
["condition", "<new|like_new|good|well_used>"],
|
||
["listing_type", "<selling|wtb>"],
|
||
["size", "<size-value>"],
|
||
["g", "<geohash>"],
|
||
["location", "<pickup location>"],
|
||
["a", "30818:<pubkey>:<spot-d-tag>"],
|
||
["available", "<when seller is available>"],
|
||
["expiration", "<unix-timestamp>"],
|
||
["status", "active"],
|
||
["published_at", "<unix-timestamp>"],
|
||
["image", "<image-url>"],
|
||
["alt", "Gear listing: <title>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Required Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `d` | Unique identifier for the listing |
|
||
| `title` | Item name |
|
||
| `t` | Must include `skateboarding` for discoverability |
|
||
| `t` | Category tag (see categories below) |
|
||
| `image` | At least one product photo |
|
||
| `price_type` | How the item is being sold: `fixed`, `free`, `trade`, `offer` |
|
||
| `listing_type` | Whether selling or buying: `selling` or `wtb` (Want to Buy) |
|
||
|
||
### Optional Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `price` | Price amount and currency (e.g., `["price", "50", "USD"]`) - required if price_type is `fixed` |
|
||
| `condition` | Item condition: `new`, `like_new`, `good`, `well_used` |
|
||
| `size` | Item size (varies by category) |
|
||
| `g` | Geohash for pickup location |
|
||
| `location` | Human-readable pickup location |
|
||
| `a` | Reference to a skate spot (format: `30818:pubkey:d-tag`) |
|
||
| `available` | When the seller is typically available |
|
||
| `expiration` | Unix timestamp when listing expires (default: 30 days) |
|
||
| `status` | Listing status: `active`, `sold`, `pending` |
|
||
| `summary` | Short one-liner about the item |
|
||
|
||
### Categories
|
||
|
||
The following `t` tags are used for categorizing gear:
|
||
|
||
- `deck` - Skateboard decks
|
||
- `trucks` - Trucks
|
||
- `wheels` - Wheels
|
||
- `bearings` - Bearings
|
||
- `shoes` - Skate shoes
|
||
- `clothing` - Clothing and apparel
|
||
- `protective` - Protective gear (helmets, pads, etc.)
|
||
- `stickers` - Stickers
|
||
- `wax` - Skate wax
|
||
- `tools` - Skate tools
|
||
- `griptape` - Griptape
|
||
- `rubbrick` - Rub bricks
|
||
|
||
### Condition Values
|
||
|
||
| Value | Label | Description |
|
||
|-------|-------|-------------|
|
||
| `new` | 🆕 New | Never used, still in packaging |
|
||
| `like_new` | 👍 Like New | Barely used, excellent condition |
|
||
| `good` | ✅ Good | Normal wear, functions perfectly |
|
||
| `well_used` | ⚠️ Well Used | Shows wear but still works |
|
||
|
||
### Price Types
|
||
|
||
| Value | Label | Description |
|
||
|-------|-------|-------------|
|
||
| `fixed` | Fixed Price | Set price in fiat or sats |
|
||
| `free` | Free | Giving away |
|
||
| `trade` | Trade | Looking to swap |
|
||
| `offer` | Make Offer | Price negotiable |
|
||
|
||
### Size Options by Category
|
||
|
||
**Decks:**
|
||
- `7.5"`, `7.75"`, `8.0"`, `8.125"`, `8.25"`, `8.38"`, `8.5"`, `8.75"`, `9.0"+`
|
||
|
||
**Trucks:**
|
||
- Sizes: `129`, `139`, `144`, `149`, `159`, etc.
|
||
- Heights: `low`, `mid`, `high`, `inverted`
|
||
|
||
**Shoes:**
|
||
- US sizes: `6`, `6.5`, `7`, `7.5`, ... `14`
|
||
- EU sizes: `38`, `39`, `40`, ... `48`
|
||
|
||
**Clothing:**
|
||
- `XS`, `S`, `M`, `L`, `XL`, `XXL`
|
||
|
||
**Wheels:**
|
||
- Diameter: `50mm`, `52mm`, `54mm`, `56mm`, `58mm`, `60mm`
|
||
- Hardness: `78a`, `87a`, `92a`, `97a`, `99a`, `101a`
|
||
|
||
### Listing Type
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `selling` | User is selling gear |
|
||
| `wtb` | Want to Buy - User is looking for specific gear |
|
||
|
||
### Expiration
|
||
|
||
Listings should include an `expiration` tag with a Unix timestamp. Default expiration periods:
|
||
- 7 days
|
||
- 14 days
|
||
- 30 days (default)
|
||
|
||
Expired listings should be filtered out by clients.
|
||
|
||
### Spot Association
|
||
|
||
Gear listings can be tied to specific skate spots using an `a` tag referencing a spot event. This allows:
|
||
- Listings to appear on spot detail pages
|
||
- Easy meetup coordination at familiar locations
|
||
- Geographic filtering of gear listings
|
||
|
||
**Important:** Gear listings should NOT appear at shop locations (conflict of interest). Only show gear at regular skate spots or dual shop+spot locations.
|
||
|
||
### NIP-05 Verification
|
||
|
||
For trust and spam prevention, sellers should have a verified NIP-05 identifier. Clients may:
|
||
- Require NIP-05 to create listings
|
||
- Display verification badges on listings
|
||
- Allow filtering by verified sellers
|
||
|
||
### Example: Selling a Deck
|
||
|
||
```json
|
||
{
|
||
"kind": 30402,
|
||
"pubkey": "abc123...",
|
||
"created_at": 1703001600,
|
||
"content": "Got this deck for Christmas but prefer a smaller size. Happy to let you try it before buying! Some minor scratches from one session but graphics are clean.",
|
||
"tags": [
|
||
["d", "listing-1703001600"],
|
||
["title", "Palace Deck 8.25\""],
|
||
["t", "skateboarding"],
|
||
["t", "deck"],
|
||
["price", "40", "USD"],
|
||
["price_type", "fixed"],
|
||
["condition", "like_new"],
|
||
["listing_type", "selling"],
|
||
["size", "8.25\""],
|
||
["g", "9q5ctr"],
|
||
["location", "Venice Beach Skatepark"],
|
||
["a", "30818:def456:venice-beach-park"],
|
||
["available", "Weekends 10am-2pm"],
|
||
["expiration", "1705593600"],
|
||
["status", "active"],
|
||
["published_at", "1703001600"],
|
||
["image", "https://example.com/deck-front.jpg"],
|
||
["image", "https://example.com/deck-back.jpg"],
|
||
["alt", "Gear listing: Palace Deck 8.25\""]
|
||
],
|
||
"id": "...",
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
### Example: Want to Buy (WTB)
|
||
|
||
```json
|
||
{
|
||
"kind": 30402,
|
||
"pubkey": "xyz789...",
|
||
"created_at": 1703001600,
|
||
"content": "Looking for a solid 8.0\" deck, any brand. Prefer minimal graphics or blank. Can pay cash or trade wheels.",
|
||
"tags": [
|
||
["d", "listing-1703001700"],
|
||
["title", "Looking for 8.0\" Deck"],
|
||
["t", "skateboarding"],
|
||
["t", "deck"],
|
||
["price_type", "offer"],
|
||
["listing_type", "wtb"],
|
||
["size", "8.0\""],
|
||
["g", "9q5ctr"],
|
||
["location", "Near Downtown Rails"],
|
||
["expiration", "1705593600"],
|
||
["status", "active"],
|
||
["published_at", "1703001600"],
|
||
["alt", "WTB: Looking for 8.0\" Deck"]
|
||
],
|
||
"id": "...",
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
### Querying Gear Listings
|
||
|
||
**All Active Listings:**
|
||
```json
|
||
{
|
||
"kinds": [30402],
|
||
"#t": ["skateboarding"]
|
||
}
|
||
```
|
||
|
||
**Listings by Category:**
|
||
```json
|
||
{
|
||
"kinds": [30402],
|
||
"#t": ["skateboarding", "deck"]
|
||
}
|
||
```
|
||
|
||
**Listings at a Spot:**
|
||
```json
|
||
{
|
||
"kinds": [30402],
|
||
"#a": ["30818:pubkey:spot-id"],
|
||
"#t": ["skateboarding"]
|
||
}
|
||
```
|
||
|
||
**Listings Near Location (using geohash prefix):**
|
||
```json
|
||
{
|
||
"kinds": [30402],
|
||
"#g": ["9q5"],
|
||
"#t": ["skateboarding"]
|
||
}
|
||
```
|
||
|
||
## Community Contributions
|
||
|
||
Community members can contribute to spots without being the owner. Contributions are stored as separate events that reference the original spot.
|
||
|
||
### Photo Contributions (kind 1063)
|
||
|
||
Community photos use NIP-94 File Metadata (kind 1063) with an `a` tag referencing the spot.
|
||
|
||
```json
|
||
{
|
||
"kind": 1063,
|
||
"content": "Great ledge for slappies!",
|
||
"tags": [
|
||
["url", "https://example.com/photo.jpg"],
|
||
["m", "image/jpeg"],
|
||
["a", "30818:<spot-pubkey>:<spot-d-tag>"],
|
||
["alt", "Community photo for Hollywood High"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Tag Suggestions (kind 1111)
|
||
|
||
Tag suggestions use NIP-22 Comments (kind 1111) with a special marker tag.
|
||
|
||
```json
|
||
{
|
||
"kind": 1111,
|
||
"content": "Suggested tags for Hollywood High: #marble #ledge #10-stair",
|
||
"tags": [
|
||
["a", "30818:<spot-pubkey>:<spot-d-tag>"],
|
||
["K", "30818"],
|
||
["t", "skatespot-tags"],
|
||
["t", "marble"],
|
||
["t", "ledge"],
|
||
["t", "10-stair"]
|
||
]
|
||
}
|
||
```
|
||
|
||
The `skatespot-tags` marker distinguishes tag suggestions from regular comments.
|
||
|
||
### Querying Contributions
|
||
|
||
**Community Photos:**
|
||
```json
|
||
{
|
||
"kinds": [1063],
|
||
"#a": ["30818:pubkey:spot-id"]
|
||
}
|
||
```
|
||
|
||
**Tag Suggestions:**
|
||
```json
|
||
{
|
||
"kinds": [1111],
|
||
"#a": ["30818:pubkey:spot-id"],
|
||
"#t": ["skatespot-tags"]
|
||
}
|
||
```
|
||
|
||
## Spot Ownership Transfer
|
||
|
||
Spot ownership can be transferred in two ways:
|
||
|
||
### Voluntary Transfer
|
||
|
||
The owner publishes a transfer request as a kind 1111 comment:
|
||
|
||
```json
|
||
{
|
||
"kind": 1111,
|
||
"content": "Ownership transfer request for spot \"Hollywood High\"",
|
||
"tags": [
|
||
["a", "30818:<owner-pubkey>:<spot-d-tag>"],
|
||
["K", "30818"],
|
||
["t", "spot-transfer"],
|
||
["p", "<new-owner-pubkey>", "", "new-owner"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Abandoned Spot Claim
|
||
|
||
If a spot hasn't been updated for 6+ months, any user can claim it by:
|
||
|
||
1. Re-publishing the spot under their own pubkey
|
||
2. Adding `claim-from` and `claim-reason` tags
|
||
|
||
```json
|
||
{
|
||
"kind": 30818,
|
||
"content": "<original description>",
|
||
"tags": [
|
||
["d", "<same-d-tag>"],
|
||
["title", "<same-title>"],
|
||
// ... other original tags ...
|
||
["claim-from", "<original-owner-pubkey>"],
|
||
["claim-reason", "abandoned"]
|
||
]
|
||
}
|
||
```
|
||
|
||
Note: Since Nostr is decentralized, the original spot remains. Clients should prefer the most recently updated version or allow users to choose.
|
||
|
||
## Skate Tours (kind 30823)
|
||
|
||
Skate tours allow experienced skaters to organize and host various types of skateboarding adventures. Tours range from local hidden spot tours to multi-country expeditions and KOTR-style World Destruction competitions.
|
||
|
||
### Tour Categories
|
||
|
||
| Category | Emoji | Description | Typical Duration |
|
||
|----------|-------|-------------|------------------|
|
||
| `local` | 🏠 | Show hidden spots in your city | 2-8 hours |
|
||
| `national` | 🚐 | Multi-city road trips across the country | 2-14 days |
|
||
| `international` | ✈️ | Cross-border adventures across multiple countries | 1-8 weeks (up to 2 months) |
|
||
| `destruction` | 💀 | World Destruction Tour - KOTR-style global team competition | 1-12 months |
|
||
|
||
### Event Format (Base)
|
||
|
||
```json
|
||
{
|
||
"kind": 30823,
|
||
"content": "<tour description - what skaters will experience>",
|
||
"tags": [
|
||
["d", "<unique-identifier>"],
|
||
["title", "<tour name>"],
|
||
["t", "skate-tour"],
|
||
["tour-category", "<local|national|international|destruction>"],
|
||
["t", "<spot-type>"],
|
||
["g", "<geohash>"],
|
||
["location", "<city/region>"],
|
||
["meeting-point", "<public meeting location name>"],
|
||
["meeting-point-coords", "<lat>", "<lon>"],
|
||
["duration", "<number>", "<hours|days|weeks>"],
|
||
["skill-level", "<beginner|intermediate|advanced|all>"],
|
||
["max-group", "<max participants>"],
|
||
["price", "<sats>"],
|
||
["available", "<availability description>"],
|
||
["languages", "<lang1>", "<lang2>"],
|
||
["shop", "<partner-shop-naddr>"],
|
||
["shop-discount", "<percentage>"],
|
||
["zap-split", "skatespots@minibits.cash", "10"],
|
||
["image", "<tour-image-url>"],
|
||
["alt", "Skate Tour: <title> in <location>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Required Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `d` | Unique identifier for the tour |
|
||
| `title` | Tour name |
|
||
| `t` | Must include `skate-tour` to identify as a tour event |
|
||
| `tour-category` | Tour type: `local`, `national`, `international`, or `destruction` |
|
||
| `location` | City or region where tour operates/starts |
|
||
|
||
### Optional Tags (All Categories)
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `g` | Geohash for location (low precision - 4 chars recommended) |
|
||
| `meeting-point` | Public meeting location name |
|
||
| `meeting-point-coords` | Latitude and longitude of meeting point |
|
||
| `duration` | Tour length with unit: `["duration", "3", "hours"]` or `["duration", "5", "days"]` |
|
||
| `skill-level` | Recommended skill level: `beginner`, `intermediate`, `advanced`, or `all` |
|
||
| `max-group` | Maximum number of participants |
|
||
| `price` | Price per person in satoshis (host decides) |
|
||
| `available` | When host is available (e.g., "Weekends", "By appointment") |
|
||
| `languages` | Languages the host speaks |
|
||
| `shop` | Reference to partner skate shop (naddr) |
|
||
| `shop-discount` | Percentage discount at partner shop for tour participants |
|
||
| `zap-split` | Platform fee recipient and percentage |
|
||
| `image` | Tour or host photo |
|
||
|
||
### Spot Type Tags
|
||
|
||
Tours can include multiple spot type tags to describe what kind of terrain will be covered:
|
||
|
||
- `street` - Street spots
|
||
- `park` - Skateparks
|
||
- `diy` - DIY spots
|
||
- `transition` - Transition/bowl spots
|
||
- `hidden` - Hidden gems
|
||
|
||
### National & International Tour Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `cities` | List of cities: `["cities", "LA", "SF", "Portland", "Seattle"]` |
|
||
| `countries` | List of countries: `["countries", "Spain", "France", "Germany"]` |
|
||
| `transport` | Transport information/recommendations |
|
||
| `accommodation` | Accommodation information/recommendations |
|
||
|
||
### World Destruction Tour Tags
|
||
|
||
World Destruction Tours are KOTR-style global team competitions where organizers define EVERYTHING - challenges, rules, prizes. The platform only provides infrastructure.
|
||
|
||
**Key Characteristics:**
|
||
- Duration: 1-12 months
|
||
- Skill level: Always **advanced** (hardcoded)
|
||
- Spot types: **ALL types included** automatically
|
||
- Challenges: **SECRET until competition start date**
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `cities` | List of competition cities: `["cities", "Berlin", "Barcelona", "Tokyo"]` |
|
||
| `countries` | List of countries: `["countries", "Germany", "Spain", "Japan"]` |
|
||
| `organizer-contact` | Organizer contact details (Nostr npub, email, Telegram, etc.) |
|
||
| `transport` | Transport information/recommendations |
|
||
| `start-date` | Unix timestamp when competition starts (**challenges revealed on this date**) |
|
||
| `end-date` | Unix timestamp when competition ends |
|
||
| `entry-fee` | Entry fee per team in satoshis (organizer decides, can be 0) |
|
||
| `prize-pool` | Total prize pool in satoshis |
|
||
| `prize-dist` | Prize distribution: `["prize-dist", "1", "50", "2", "30", "3", "20"]` (place, percentage pairs) |
|
||
| `max-teams` | Maximum number of teams allowed |
|
||
| `min-team-size` | Minimum skaters per team |
|
||
| `max-team-size` | Maximum skaters per team |
|
||
| `challenge` | Challenge definition (multiple allowed): `["challenge", "id", "title", "description", "points", "revealDate"]` |
|
||
| `clip-sources` | Accepted clip sources: `["clip-sources", "blossom", "youtube", "instagram", "any"]` |
|
||
|
||
### Challenge Tag Format
|
||
|
||
```json
|
||
["challenge", "<id>", "<title>", "<description>", "<points>", "<revealDate>"]
|
||
```
|
||
|
||
- `id`: Unique challenge identifier
|
||
- `title`: Challenge name (e.g., "Kickflip a 10 stair")
|
||
- `description`: Optional detailed description
|
||
- `points`: Points awarded for completion
|
||
- `revealDate`: Unix timestamp when challenge becomes visible (defaults to tour start date - **ALL challenges are SECRET until this date**)
|
||
|
||
### Revenue Model
|
||
|
||
**Hosts set their own prices.** Platform commission varies by tour type via `zap-split` tag.
|
||
|
||
| Tour Type | Platform Fee | Host Keeps |
|
||
|-----------|--------------|------------|
|
||
| Local | 10% | 90% |
|
||
| National | 7% | 93% |
|
||
| International | 5% | 95% |
|
||
| World Destruction | 5% | 95% |
|
||
|
||
**Important:** For World Destruction Tours, the 5% fee applies only to **entry fees**, NOT to sponsor money or prize pools.
|
||
|
||
Example zap-split tags:
|
||
- Local: `["zap-split", "skatespots@minibits.cash", "10"]`
|
||
- National: `["zap-split", "skatespots@minibits.cash", "7"]`
|
||
- International/Destruction: `["zap-split", "skatespots@minibits.cash", "5"]`
|
||
|
||
### Sponsor Tags
|
||
|
||
Tours can have sponsors. Sponsors are stored as tags and displayed on tour pages.
|
||
|
||
```json
|
||
["sponsor", "<tier>", "<name>", "<logo-url>", "<website-url>", "<challengeId?>"]
|
||
```
|
||
|
||
| Field | Description |
|
||
|-------|-------------|
|
||
| `tier` | Sponsor level: `title`, `gold`, `silver`, `challenge`, `partner` |
|
||
| `name` | Sponsor name (required) |
|
||
| `logo-url` | URL to sponsor logo image (optional) |
|
||
| `website-url` | Sponsor website URL (optional) |
|
||
| `challengeId` | For challenge sponsors only - which challenge they sponsor |
|
||
|
||
**Sponsor Tiers:**
|
||
- `title` - Title/naming sponsor (e.g., "The Vans World Destruction Tour")
|
||
- `gold` - Major sponsor with prominent placement
|
||
- `silver` - Secondary sponsor
|
||
- `challenge` - Sponsors a specific challenge
|
||
- `partner` - General partner/supporter
|
||
|
||
Example:
|
||
```json
|
||
["sponsor", "title", "Vans", "https://vans.com/logo.png", "https://vans.com"],
|
||
["sponsor", "gold", "Monster Energy", "https://monster.com/logo.png", "https://monster.com"],
|
||
["sponsor", "challenge", "Red Bull", "https://redbull.com/logo.png", "https://redbull.com", "ch3"]
|
||
```
|
||
|
||
### Privacy Model
|
||
|
||
**Critical**: Hidden spots are NEVER published to Nostr. They are shared in-person during the tour only.
|
||
|
||
- Meeting point is a known public location
|
||
- City-level geohash provides general location for discovery
|
||
- Actual hidden spots are revealed only during the tour
|
||
|
||
### Video/Clip Submissions
|
||
|
||
For destruction tours, organizers can accept clip submissions as proof of challenge completion:
|
||
|
||
1. **Blossom uploads** - For clips under 50MB
|
||
2. **External links** - YouTube, Instagram, or any URL
|
||
|
||
Organizers review submissions and award points manually. The platform does NOT host video - it only stores references.
|
||
|
||
### Example: Local Tour
|
||
|
||
```json
|
||
{
|
||
"kind": 30823,
|
||
"pubkey": "abc123...",
|
||
"created_at": 1703001600,
|
||
"content": "Join me for a 3-hour street spot tour of Barcelona's Gothic Quarter.",
|
||
"tags": [
|
||
["d", "barcelona-gothic-hidden-tour"],
|
||
["title", "Barcelona Gothic Quarter - Hidden Gems Tour"],
|
||
["t", "skate-tour"],
|
||
["tour-category", "local"],
|
||
["t", "street"],
|
||
["t", "hidden"],
|
||
["g", "sp3e"],
|
||
["location", "Barcelona, Spain"],
|
||
["meeting-point", "MACBA Museum Steps"],
|
||
["meeting-point-coords", "41.3833", "2.1670"],
|
||
["duration", "3", "hours"],
|
||
["skill-level", "intermediate"],
|
||
["max-group", "4"],
|
||
["price", "25000"],
|
||
["languages", "en", "es", "ca"],
|
||
["zap-split", "skatespots@minibits.cash", "10"],
|
||
["alt", "Skate Tour: Barcelona Gothic Quarter in Barcelona, Spain"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Example: National Tour
|
||
|
||
```json
|
||
{
|
||
"kind": 30823,
|
||
"pubkey": "def456...",
|
||
"created_at": 1703001600,
|
||
"content": "5-day California coast tour hitting the best spots from LA to SF. Van included, bring your own board!",
|
||
"tags": [
|
||
["d", "california-coast-tour-2024"],
|
||
["title", "California Coast Tour 2024"],
|
||
["t", "skate-tour"],
|
||
["tour-category", "national"],
|
||
["t", "street"],
|
||
["t", "park"],
|
||
["g", "9q5"],
|
||
["location", "California, USA"],
|
||
["cities", "Los Angeles", "Santa Barbara", "San Luis Obispo", "Santa Cruz", "San Francisco"],
|
||
["meeting-point", "Venice Beach Skatepark"],
|
||
["meeting-point-coords", "33.9850", "-118.4695"],
|
||
["duration", "5", "days"],
|
||
["skill-level", "intermediate"],
|
||
["max-group", "8"],
|
||
["price", "500000"],
|
||
["transport", "15-passenger van, driver included"],
|
||
["accommodation", "Budget hostels along the route - not included in price"],
|
||
["languages", "en", "es"],
|
||
["zap-split", "skatespots@minibits.cash", "10"],
|
||
["alt", "Skate Tour: California Coast Tour 2024"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Example: World Destruction Tour
|
||
|
||
```json
|
||
{
|
||
"kind": 30823,
|
||
"pubkey": "ghi789...",
|
||
"created_at": 1703001600,
|
||
"content": "EUROPEAN WORLD DESTRUCTION TOUR 2024. 4 teams battle across Europe's gnarliest spots for 3 months. Complete challenges, film clips, earn points. Winner takes all!",
|
||
"tags": [
|
||
["d", "europe-destruction-2024"],
|
||
["title", "European World Destruction Tour 2024"],
|
||
["t", "skate-tour"],
|
||
["tour-category", "destruction"],
|
||
["t", "street"],
|
||
["t", "park"],
|
||
["t", "diy"],
|
||
["t", "transition"],
|
||
["t", "hidden"],
|
||
["cities", "Berlin", "Barcelona", "Paris", "London", "Amsterdam"],
|
||
["countries", "Germany", "Spain", "France", "UK", "Netherlands"],
|
||
["organizer-contact", "DM on Nostr: npub1... or email: destruction@skatespots.org"],
|
||
["transport", "Teams arrange own transport between cities"],
|
||
["duration", "12", "weeks"],
|
||
["skill-level", "advanced"],
|
||
["start-date", "1720000000"],
|
||
["end-date", "1727776000"],
|
||
["entry-fee", "100000"],
|
||
["prize-pool", "5000000"],
|
||
["prize-dist", "1", "50", "2", "30", "3", "20"],
|
||
["max-teams", "8"],
|
||
["min-team-size", "4"],
|
||
["max-team-size", "6"],
|
||
["challenge", "ch1", "First kickflip at Warschauer", "Land a kickflip on the famous Warschauer banks", "100", "1720000000"],
|
||
["challenge", "ch2", "Get a local to join", "Convince a local skater to join your session", "200", "1720000000"],
|
||
["challenge", "ch3", "Fountain trick", "Land a trick into the Alexanderplatz fountain", "500", "1720000000"],
|
||
["challenge", "ch4", "3-country video part", "Film a video part spanning at least 3 countries", "1000", "1720000000"],
|
||
["challenge", "ch5", "MACBA line", "Film a full line at MACBA Barcelona", "777", "1720000000"],
|
||
["clip-sources", "blossom", "youtube", "instagram", "any"],
|
||
["languages", "en", "de", "es", "fr"],
|
||
["zap-split", "skatespots@minibits.cash", "10"],
|
||
["alt", "World Destruction Tour: European World Destruction Tour 2024"]
|
||
]
|
||
}
|
||
```
|
||
|
||
**Note:** All challenges have the same `revealDate` (1720000000) which is the tour start date. Teams cannot see challenge details until this date.
|
||
|
||
### Querying Tours
|
||
|
||
**All Tours:**
|
||
```json
|
||
{
|
||
"kinds": [30823],
|
||
"#t": ["skate-tour"]
|
||
}
|
||
```
|
||
|
||
**Tours by Category:**
|
||
```json
|
||
{
|
||
"kinds": [30823],
|
||
"#t": ["skate-tour"]
|
||
}
|
||
```
|
||
Note: Filter by `tour-category` tag client-side.
|
||
|
||
**Tours in a City (by geohash prefix):**
|
||
```json
|
||
{
|
||
"kinds": [30823],
|
||
"#t": ["skate-tour"],
|
||
"#g": ["sp3"]
|
||
}
|
||
```
|
||
|
||
**Tours by a Host:**
|
||
```json
|
||
{
|
||
"kinds": [30823],
|
||
"authors": ["<host-pubkey>"],
|
||
"#t": ["skate-tour"]
|
||
}
|
||
```
|
||
|
||
## Food Recommendations
|
||
|
||
Food spots near skate locations are handled client-side by querying external APIs (like OpenStreetMap/Overpass) using the geohash coordinates from skate spots.
|
||
|
||
## Private Stash (kind 30001)
|
||
|
||
The Stash feature uses NIP-51 Follow Sets (kind 30001) to store a user's private gear inventory. The content is NIP-44 encrypted to self, making it private by default with selective disclosure options.
|
||
|
||
### Event Format
|
||
|
||
```json
|
||
{
|
||
"kind": 30001,
|
||
"content": "<NIP-44 encrypted JSON>",
|
||
"tags": [
|
||
["d", "skatespots-stash"],
|
||
["alt", "Private gear stash (encrypted)"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Encrypted Content Structure
|
||
|
||
When decrypted, the content contains a JSON object with the user's inventory:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"id": "<uuid>",
|
||
"title": "Powell Peralta Flight Deck 8.25",
|
||
"category": "deck",
|
||
"condition": "like_new",
|
||
"size": "8.25\"",
|
||
"brand": "Powell Peralta",
|
||
"description": "Barely used, no chips or cracks",
|
||
"minPrice": 50000,
|
||
"maxPrice": null,
|
||
"images": ["https://example.com/deck.jpg"],
|
||
"willingToTrade": true,
|
||
"willingToSell": true,
|
||
"addedAt": 1703001600,
|
||
"visibility": "friends",
|
||
"itemType": "stash",
|
||
"rarity": "rare"
|
||
},
|
||
{
|
||
"id": "<uuid>",
|
||
"title": "Looking for 8.0\" deck",
|
||
"category": "deck",
|
||
"condition": "good",
|
||
"size": "8.0\"",
|
||
"brand": "",
|
||
"description": "Any brand, prefer minimal graphics",
|
||
"minPrice": null,
|
||
"maxPrice": 100000,
|
||
"images": [],
|
||
"willingToTrade": true,
|
||
"willingToSell": false,
|
||
"addedAt": 1703002600,
|
||
"visibility": "friends",
|
||
"itemType": "wtb",
|
||
"rarity": null
|
||
}
|
||
],
|
||
"updatedAt": 1703002600
|
||
}
|
||
```
|
||
|
||
### Item Types
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `stash` | Regular inventory item that the user owns |
|
||
| `wtb` | Want To Buy - item the user is looking for |
|
||
|
||
### Visibility Levels
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `private` | Only visible to the owner. Shared manually via DM. |
|
||
| `friends` | Visible to mutual follows. Enables P2P matching. |
|
||
| `public` | Visible to anyone who views the user's profile. |
|
||
|
||
### Categories
|
||
|
||
Same as Gear Listings: `deck`, `trucks`, `wheels`, `bearings`, `shoes`, `clothing`, `protective`, `complete`, `other`
|
||
|
||
### Conditions
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `new` | Brand new, never used |
|
||
| `like_new` | Barely used, excellent condition |
|
||
| `good` | Normal wear, functions perfectly |
|
||
| `well_used` | Shows wear but still works |
|
||
|
||
### Rarity (Optional)
|
||
|
||
Items can have an optional rarity tier for RPG-style display:
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `common` | Standard items |
|
||
| `uncommon` | Slightly rare brands/models |
|
||
| `rare` | Hard to find items |
|
||
| `epic` | Very rare or valuable items |
|
||
| `legendary` | Extremely rare collector items |
|
||
|
||
### WTB Matching
|
||
|
||
The app performs client-side matching between WTB items and stash items from friends (mutual follows). Matches are based on:
|
||
|
||
- Category (required match)
|
||
- Brand (fuzzy match)
|
||
- Size (within tolerance)
|
||
- Price compatibility (WTB maxPrice >= stash minPrice)
|
||
|
||
When a match is found, the app can facilitate P2P trading via encrypted DMs (NIP-17).
|
||
|
||
### Privacy Model
|
||
|
||
- All stash data is NIP-44 encrypted to self
|
||
- Private items are never shared without explicit user action
|
||
- "Friends" visibility items can be queried by mutual follows through a discovery protocol
|
||
- WTB matching happens client-side with no central server
|
||
|
||
### Security Considerations
|
||
|
||
1. The stash is encrypted at rest using the user's Nostr keys
|
||
2. Selective disclosure allows users to share specific items via DM
|
||
3. No price or inventory data is publicly visible unless explicitly set to "public"
|
||
4. Group DMs (NIP-17) can be used for coordinating trades between multiple parties
|
||
|
||
## Rider Cards
|
||
|
||
SkateSpots supports physical-print cards that are cryptographically signed on Nostr and verifiable by anyone.
|
||
|
||
### Park Access Card (kind 35679)
|
||
|
||
Issued by a private skatepark owner to a specific rider, granting access rights. The park's Nostr key signs the event — anyone can verify the card is genuine by scanning the QR code.
|
||
|
||
**Constraint:** Park Access Cards for private parks (`access=private` in kind 30818) should only be issued by the park's owner pubkey. Clients verify the issuer's pubkey matches the park event's `pubkey` field.
|
||
|
||
```json
|
||
{
|
||
"kind": 35679,
|
||
"content": "",
|
||
"tags": [
|
||
["d", "<unique-id>"],
|
||
["p", "<rider-pubkey>"],
|
||
["t", "skatepark-access"],
|
||
["issuer", "<park name>"],
|
||
["access-level", "<Full Access|Supervised|Beginner Zone|Street Only|Bowl Only|Members Only>"],
|
||
["theme", "<thug|lime|chrome|wood|midnight|sunset>"],
|
||
["valid-thru", "<YYYY-MM-DD>"],
|
||
["alt", "Skatepark Access Card"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Card Themes
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `thug` | Black + Neon Red (default) |
|
||
| `lime` | Black + Neon Lime |
|
||
| `chrome` | Steel & Silver |
|
||
| `wood` | Woodgrain Classic |
|
||
| `midnight` | Deep Blue |
|
||
| `sunset` | Orange & Pink |
|
||
|
||
### Rider License (client-side only)
|
||
|
||
The personal Rider License is generated client-side from the user's kind 0 profile metadata and on-chain stats. It is not a separate Nostr event — it renders from existing data:
|
||
|
||
- Name, avatar, NIP-05: from kind 0
|
||
- Spots added: count of kind 30818 authored by the user
|
||
- Zaps received: count of kind 9735 targeting the user
|
||
- Events created: count of kind 31923 authored by the user
|
||
- Park cards held: count of kind 35679 with `#p` = user
|
||
- Witnessed skill badges: count of kind 1293 with `#p` = user
|
||
|
||
The Rider License includes two categories of skill badges:
|
||
- **Self-claimed** — selected by the rider themselves, shown in theme color
|
||
- **Witnessed** — awarded by others (kind 1293), shown in gold ⭐
|
||
|
||
The "Rides For" field displays the local shop the rider represents, distinct from sponsors (brands).
|
||
|
||
### Verification
|
||
|
||
The QR code on printed Park Access cards encodes the Nostr address (`naddr`) of the issuing event. Verifiers scan the QR, fetch the event from any relay, and confirm the issuer's signature.
|
||
|
||
```
|
||
URL format: https://skate-spots.org/verify/<naddr>
|
||
```
|
||
|
||
## Skill Badge Awards (kind 1293)
|
||
|
||
Anyone who witnesses a rider performing a trick can award them a cryptographically signed skill badge. This creates an on-chain proof of skill — unfakeable because the witness's Nostr key signs the event.
|
||
|
||
### Design Rationale
|
||
|
||
Skill badges are **regular events** (not addressable) because:
|
||
- Multiple witnesses can each award the same skill to the same rider independently
|
||
- Each badge is a distinct attestation with its own timestamp and witness
|
||
- The full history of attestations is valuable (not just the latest)
|
||
|
||
### Event Format
|
||
|
||
```json
|
||
{
|
||
"kind": 1293,
|
||
"content": "",
|
||
"tags": [
|
||
["p", "<rider-pubkey>"],
|
||
["skill", "<skill-id>"],
|
||
["skill", "<skill-id>"],
|
||
["spot", "<spot name (optional)>"],
|
||
["t", "skill-badge"],
|
||
["alt", "Skill badge awarded: <skill>, <skill>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Required Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `p` | Pubkey of the rider receiving the badge |
|
||
| `skill` | Skill identifier (one tag per skill, multiple allowed) |
|
||
| `t` | Must be `skill-badge` for discoverability |
|
||
| `alt` | NIP-31 human-readable description |
|
||
|
||
### Optional Tags
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `spot` | Name of the spot where the trick was witnessed |
|
||
| `a` | Reference to a kind 30818 spot event |
|
||
|
||
### Skill Identifiers
|
||
|
||
| Value | Label |
|
||
|-------|-------|
|
||
| `kickflip` | Kickflip |
|
||
| `heelflip` | Heelflip |
|
||
| `switch` | Switch stance |
|
||
| `nollie` | Nollie |
|
||
| `fakie` | Fakie |
|
||
| `grind` | Grinds |
|
||
| `slide` | Slides |
|
||
| `manual` | Manuals |
|
||
| `vert` | Vert |
|
||
| `bowl` | Bowl |
|
||
| `street` | Street |
|
||
| `park` | Park |
|
||
| `film` | Filmer |
|
||
| `diy` | DIY Builder |
|
||
|
||
### Trust Model
|
||
|
||
The value of a badge is proportional to the reputation of the witness:
|
||
- NIP-05 verified witnesses carry more weight
|
||
- Witnesses known in the community (follows, mutual follows) carry more weight
|
||
- The skill badge feed is public — anyone can query badges for any rider
|
||
|
||
### Querying
|
||
|
||
**All badges received by a rider:**
|
||
```json
|
||
{
|
||
"kinds": [1293],
|
||
"#p": ["<rider-pubkey>"],
|
||
"#t": ["skill-badge"]
|
||
}
|
||
```
|
||
|
||
**All badges issued by a witness:**
|
||
```json
|
||
{
|
||
"kinds": [1293],
|
||
"authors": ["<witness-pubkey>"],
|
||
"#t": ["skill-badge"]
|
||
}
|
||
```
|
||
|
||
**Badges for a specific skill:**
|
||
```json
|
||
{
|
||
"kinds": [1293],
|
||
"#p": ["<rider-pubkey>"],
|
||
"#skill": ["kickflip"]
|
||
}
|
||
```
|
||
|
||
### Example
|
||
|
||
```json
|
||
{
|
||
"kind": 1293,
|
||
"pubkey": "witness-pubkey-hex",
|
||
"created_at": 1703001600,
|
||
"content": "",
|
||
"tags": [
|
||
["p", "rider-pubkey-hex"],
|
||
["skill", "kickflip"],
|
||
["skill", "switch"],
|
||
["spot", "MACBA Barcelona"],
|
||
["a", "30818:abc123:macba"],
|
||
["t", "skill-badge"],
|
||
["alt", "Skill badge awarded: kickflip, switch"]
|
||
],
|
||
"id": "...",
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
## The Lab — Service Profiles
|
||
|
||
The Lab connects skaters with service providers. Each provider type is an
|
||
addressable event (kind 35001–35004) identified by `pubkey + kind + d-tag`.
|
||
They share a common base schema and are distinguished by kind and a self-tag.
|
||
|
||
| Kind | Provider | Self-tag |
|
||
|------|----------|----------|
|
||
| 35001 | Coach | `skatecoach` |
|
||
| 35002 | Filmer (video) | `skatefilmer` |
|
||
| 35003 | Video Editor | `skateeditor` |
|
||
| 35004 | Photographer | `skatephotographer` |
|
||
|
||
### Common Base Schema
|
||
|
||
```json
|
||
{
|
||
"kind": 35004,
|
||
"content": "<bio / about the provider>",
|
||
"tags": [
|
||
["d", "<profile-identifier>"],
|
||
["title", "<display name>"],
|
||
["t", "<self-tag>"],
|
||
["t", "<service/skill/gear tag>"],
|
||
["location", "<city / area>"],
|
||
["g", "<geohash>"],
|
||
["image", "<profile image url>"],
|
||
["experience", "<years>"],
|
||
["rate", "<freeform rate, e.g. 20000 sats/shoot>"],
|
||
["phone", "<phone>"],
|
||
["email", "<email>"],
|
||
["website", "<url>"],
|
||
["a", "30818:<pubkey>:<d-tag>"],
|
||
["alt", "<human-readable description>"]
|
||
]
|
||
}
|
||
```
|
||
|
||
#### Required Tags
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `d` | Unique profile identifier (e.g. `photography-profile`) |
|
||
| `title` | Display name |
|
||
| `t` | Must include the provider self-tag (see table above) |
|
||
|
||
#### Shared Optional Tags
|
||
`location`, `g` (geohash), `image`, `experience`, `rate`, `phone`, `email`,
|
||
`website`, `a` (spot references to kind 30818), `alt`.
|
||
|
||
### Photographer (kind 35004)
|
||
|
||
Skate photography is a distinct craft from filming — it is shot-, light-, and
|
||
print-oriented. A photographer profile records services and gear as queryable
|
||
`t` tags, plus a freeform `gear` tag and a `portfolio`/`social` link.
|
||
|
||
Categories encoded as `t` tags:
|
||
|
||
- **Services**: `action`, `sequence`, `portraits`, `editorial`, `product`, `events`, `lifestyle`, `prints`
|
||
- **Camera bodies**: `dslr`, `mirrorless`, `medium`, `film35`, `filmmedium`, `point`, `phone`
|
||
- **Lenses**: `fisheye`, `wide`, `standard`, `telephoto`, `macro`, `zoom`
|
||
- **Lighting**: `strobes`, `speedlights`, `profoto`, `elinchrom`, `godox`, `triggers`, `continuous`, `reflectors`, `naturallight`
|
||
- **Support**: `tripod`, `monopod`, `lightstands`, `gimbal`, `bag`, `laptop`
|
||
- **Style**: `flashfisheye`, `natural`, `bw`, `film`, `longlens`, `studio`, `street`
|
||
|
||
Photographer-specific optional tags:
|
||
|
||
| Tag | Description |
|
||
|-----|-------------|
|
||
| `gear` | Freeform "what's in the bag" (exact bodies, lenses, lights) |
|
||
| `portfolio` | Portfolio link (Instagram, personal site, etc.) |
|
||
| `social` | Additional social handle or link |
|
||
|
||
#### Example: Photographer
|
||
|
||
```json
|
||
{
|
||
"kind": 35004,
|
||
"content": "Shooting street and park sessions for 8 years. Love a lit flash fisheye at dusk.",
|
||
"tags": [
|
||
["d", "photography-profile"],
|
||
["title", "Ana Flash"],
|
||
["t", "skatephotographer"],
|
||
["t", "action"], ["t", "sequence"], ["t", "editorial"],
|
||
["t", "mirrorless"], ["t", "fisheye"], ["t", "telephoto"],
|
||
["t", "godox"], ["t", "triggers"],
|
||
["t", "tripod"], ["t", "lightstands"],
|
||
["t", "flashfisheye"], ["t", "bw"],
|
||
["location", "Barcelona, ES"],
|
||
["g", "sp3e9"],
|
||
["experience", "8"],
|
||
["rate", "25000 sats/shoot"],
|
||
["gear", "Sony A7 IV, 15mm fisheye, 70-200mm f/2.8, 2× Godox AD200 Pro, remote triggers, Manfrotto tripod"],
|
||
["portfolio", "https://instagram.com/anaflash"],
|
||
["alt", "Skate photographer: Ana Flash"]
|
||
]
|
||
}
|
||
```
|
||
|
||
### Querying Lab Profiles
|
||
|
||
```json
|
||
{
|
||
"kinds": [35001, 35002, 35003, 35004],
|
||
"#t": ["skatecoach", "skatefilmer", "skateeditor", "skatephotographer"],
|
||
"limit": 100
|
||
}
|
||
```
|