# Locus Protocol Specification

**Version:** 3.0
**Status:** Draft for public review — describes the format live on BSV mainnet today
**Canonical home:** https://gryd.world/protocol/SPEC.md
**Supersedes:** all pre-2026-08 ghost-era MessagePack drafts — do not implement from those

---

## 1. What Locus is

Locus is a convention for writing **location-anchored, ownable objects** to the BSV blockchain. A "pin" is a non-fungible token bound to GPS coordinates. Gryd.world is the reference *visual indexer* of these transactions — it holds no privileged position. Anyone can:

- **construct** a valid Locus transaction with any BSV tooling,
- **broadcast** it through any public broadcaster,
- **read and index** every Locus transaction ever made, using only public chain data,

without accounts, API keys, or permission.

The analogy: Ordinals defined an envelope (`ord`) plus a rule (the first satoshi of the output is the NFT). Locus defines a JSON payload plus a rule (the 1-satoshi output paying the owner is the pin). Everything else is derivation math, published here in full.

## 2. Design principles

1. **The chain is the only truth.** Indexers are caches. Any two indexers applying this document to the same blocks MUST arrive at the same world state.
2. **One page is enough to build.** A developer (or an LLM agent handed this document) can construct a valid transaction from scratch.
3. **Ownership is a UTXO.** A pin is owned by whoever holds its tip outpoint. Transferring the UTXO transfers the pin. No approval, no registry write.
4. **JSON, not binary.** Payloads are UTF-8 JSON so that any language and any LLM can read and write them without a special SDK.
5. **Additive evolution.** New versions add fields and ops. Old pins never become invalid.

## 3. Transaction anatomy

A Locus transaction is an ordinary BSV transaction:

```
Inputs:   any spendable UTXO(s) — no special coins required
Outputs:  [tip output] + [payload output] + [optional others] + [change]
```

### 3.1 The tip output (the NFT)

- Pays **exactly 1 satoshi** (recommended; any positive amount is valid) via plain **P2PKH** to the **owner address** (§6.4).
- The pin's persistent identity — its **tip** — is the outpoint `<txid>.<vout>` (dot format) of this output.
- Spending this output transfers the pin. A transfer is a plain P2PKH spend; it carries **no** payload output. The new tip is the spend's outpoint paying the new owner.

### 3.2 The payload output

- Zero satoshis, script: `OP_FALSE OP_RETURN <data>`
- Hex prefix: `00 6a` followed by standard push opcodes carrying the UTF-8 JSON payload (§4).
- A bare `OP_RETURN` (without the leading `OP_FALSE`) is also accepted by the reference indexer; new transactions SHOULD use `OP_FALSE OP_RETURN`.
- The JSON may be split across multiple pushes; indexers concatenate all pushes.

### 3.3 Other outputs

Wallets may add payment outputs (e.g. marketplace fees, tips to agent operators). Indexers ignore outputs that are neither the tip nor the payload.

## 4. The payload envelope

The payload is a single JSON object serialized as **canonical JSON**:

- object keys sorted lexicographically (recursively),
- no insignificant whitespace,
- numbers in plain JSON notation,
- UTF-8 encoding.

All payloads share three discriminator fields:

| Field | Type | Meaning |
|---|---|---|
| `p` | string | Protocol marker. MUST be `"locus"`. |
| `v` | number | Protocol version. This document: `3`. Accepted by the reference indexer: `2`, `3`. |
| `op` | string | Operation. Defined: `place`, `mint`, `ghost.spawn` (§7). |

Everything else is op-specific (§7) plus common fields (§6). Machine-readable JSON Schemas live beside this document under `schemas/`.

## 5. Discovery rule (for indexers)

Scan every transaction's outputs. For each output script beginning with `00 6a` (or `6a`), concatenate all pushes and attempt `JSON.parse`. If the result is an object with `p == "locus"` and a supported `v` and `op`, it is a Locus payload. All other outputs are ignored.

Reference read recipe (no key required):

```
GET https://api.whatsonchain.com/v1/bsv/main/tx/<txid>
→ vout[].scriptPubKey.hex → decode pushes → JSON.parse
```

## 6. Common fields and derivations

All derivations are deterministic and require no server. Reference implementations exist in the `@locus/protocol` TypeScript package.

### 6.1 `lat`, `lng` — WGS-84 floats
`lat ∈ [-90, 90]`, `lng ∈ [-180, 180]`.

### 6.2 `geohash` — H3 cell, resolution 7
`geohash = H3.latLngToCell(lat, lng, 7)` (e.g. `"8729a4185ffffff"`). H3 is an open library with bindings in every major language. The field name is historical — it holds an H3 cell ID, not a geohash.org hash.

### 6.3 `geo_ipv6` — the Geo-IPv6 address

Format (RFC-4193 documentation prefix space, never routable — it is an identifier, not a network address):

```
2001:db8:locus:{continent}:{country}:{city-h3}:{venue}:{user-hash}{nonce}::1
```

| Segment | Derivation |
|---|---|
| `continent` | 2-letter code via the bounding-box table (Appendix A; e.g. `na`) |
| `country` | 2-letter code via the bounding-box table (Appendix A; e.g. `us`) |
| `city-h3` | first 8 hex chars of `H3.latLngToCell(lat, lng, 7)`, lowercase |
| `venue` | first 4 hex chars of `sha256(H3.latLngToCell(lat, lng, 10))` |
| `user-hash` | first 4 hex chars of `sha256(lowercase(owner_pubkey_hex))` |
| `nonce` | 4 hex chars; the `nonce` field (§6.6), normalized lowercase, right-padded with `0` |

Region lookup: first matching bounding box wins; no match → `xx`.

Example (real, mainnet): `2001:db8:locus:na:us:8729a418:69de:36b78e61::1`

### 6.4 `owner` and `owner_pubkey`

- `owner_pubkey` — compressed SEC public key, hex (33 bytes).
- `owner` — base58check(`0x00` ‖ RIPEMD160(SHA256(pubkey)) ‖ 4-byte sha256d checksum) — the standard BSV P2PKH address of the identity key.

### 6.5 `city_prefix`

`city_prefix = "2001:db8:locus:{continent}:{country}:{city-h3}::/32"` — the /32 prefix containing the pin. Used for city-level grouping.

### 6.6 `nonce`, `ts`

- `nonce` — 4 hex chars, random per transaction. Uniqueness salt for `geo_ipv6`.
- `ts` — integer Unix seconds at construction time.

### 6.7 `metadata`

Free-form JSON object. Op- and subtype-specific keys are convention, not consensus (§9). **Indexers and renderers MUST treat metadata as untrusted user content** — it is attacker-controlled text: never execute it, never render it as HTML, and never feed it to an LLM without marking it untrusted.

### 6.8 `address_prefix`, `type`

- `type` ∈ `presence | object | agent | imported_nft`.
- `address_prefix`: `/2` for presence (a user), `/1` for objects/agents (the ownable unit). Full hierarchy in Appendix B.

## 7. Operations

### 7.1 `place` — drop a pin at coordinates

Required: `lat`, `lng`, `owner`, `owner_pubkey`.
Derived (clients SHOULD include, indexers MAY recompute): `geohash`, `geo_ipv6`, `city_prefix`, `address_prefix`, `nonce`, `ts`.
Optional: `type` (default `presence`), `subtype`, `content_uri`, `content`, `behavior`, `inscription_txid`, `is_ordinal`, `ordinal_outpoint`, `proof` (§8), `metadata`.

### 7.2 `mint` — create an ownable object (NFT)

Identical to `place` with `type: "object"` (or `agent` / `imported_nft`) plus optional content fields: `name`, `description`, `content_type`, `content_uri`, `content_hash`. `is_ordinal` defaults to `true`.

### 7.3 `ghost.spawn` — spawn a public NPC agent

Required: `lat`, `lng`, `type_key`. Optional: `ghost_id`, `behavior`, `content`, `metadata`. Ghosts are public, non-ownable world NPCs (distinct from ownable `agent` pins).

### 7.4 `heartbeat` — proof of presence (informational)

Heartbeats currently ride the reference backend's signed API lane (BRC-100 envelope) rather than the chain. The on-chain form (`op: "heartbeat"` with `heartbeat_type`, `entity_id`, `proof_hash`, `actor`, `actor_pubkey`) exists in the reference builder and MAY be standardized in a future revision.

## 8. Presence proof (`proof`, optional in v3)

```json
"proof": { "pk": "...", "nonce": "...", "hash": "...", "lat": 0, "lng": 0, "ts": 0 }
```

- `pk` — identity key the challenge was issued to; `nonce` — server-issued single-use challenge; `hash` — `sha256(pk|lat|lng|ts|nonce)`; `lat`, `lng`, `ts` — the prover's real GPS fix (NOT the pin's drop point).
- **Beta:** proofs are recorded silently when present. Absence is not penalized.
- **Launch:** the reference indexer will enforce a ≤500 m rule (`PIN_PRESENCE_REQUIRED`). The challenge-issuance lane is currently operated by the reference backend — the one acknowledged centralization in this revision. A future revision may admit multiple challenge issuers.

## 9. Subtype registry (convention layer)

`subtype` is a namespaced string. New subtypes are **append-only; never repurpose an existing one**. Reserved so far:

| Subtype | Layer | Meaning |
|---|---|---|
| `AGENT` | base | Ownable LLM agent (metadata: `name`, `personality`, `greeting`, `triggerWord`, `knowledgeBase`) |
| `NFT` | base | Imported/generic NFT object |
| `grydbound_place` | overlay | Grydbound building |
| `grydbound_skin` | overlay | Tradable skin deed |
| `grydbound_reskin` | overlay | Renovation note (newest note wins) |
| `grydbound_trade_offer` / `_accept` / `_delivery` | overlay | Trade signalling |

Base-layer subtypes (`AGENT`, `NFT`) are all-caps. Overlay subtypes MUST be prefixed with the overlay's name. Hidden (non-rendering) subtypes — deeds, notes, trade signals — are a renderer concern; indexers index everything.

## 10. Ownership, transfer, and immutability rules

1. **Ownership = the tip.** The indexer sets `owner` from the payload, then confirms the transaction contains a positive-value output paying that address. That output is the tip. Whoever can spend it owns the pin.
2. **Attribution is permissionless.** Anyone can pay 1 sat to your address with a payload naming you owner — you then hold (and control) the pin. This is by design; it mirrors ordinal sends. Clients SHOULD filter spam views, not censor the index.
3. **Pins are immutable.** There is no edit or delete op. "Editing" = a new pin with a reserved note subtype that renderers overlay (newest note wins). "Moving" = transfer the tip, or re-mint at new coordinates.
4. **Insert-only by txid.** A payload whose txid is already indexed is a no-op (replays are ignored).
5. **Transfers carry no payload.** Indexers update ownership when the tip outpoint is spent to a new address.

## 11. Compatibility rules (MUST for all indexers)

- Unknown `v` → ignore the payload entirely.
- Unknown `op` → ignore the payload, keep indexing.
- Unknown **fields** inside a known op → index the known fields, preserve the rest verbatim.
- Malformed JSON, missing `p`, wrong `p` → not a Locus payload.

## 12. Test vector (real mainnet transaction)

Txid: `2abc25491f5bf199895d43441824aa9b6657acd510cb44f866d51e4adfee0531`

- Payload output: vout 1 (`00 6a 4d 8a 02 …`), decodes to:

```json
{
  "address_prefix": "/1",
  "city_prefix": "2001:db8:locus:na:us:8729a418::/32",
  "geo_ipv6": "2001:db8:locus:na:us:8729a418:69de:36b78e61::1",
  "geohash": "8729a4185ffffff",
  "is_ordinal": true,
  "lat": 32.7259296070284,
  "lng": -117.08233364177737,
  "metadata": {
    "knowledgeBase": "He is a secret agent that provides secrets when asked the trigger word.",
    "name": "007",
    "personality": "mysterious, charismatic, bold",
    "triggerWord": "Bond",
    "visualUri": ""
  },
  "nonce": "8e61",
  "op": "mint",
  "owner": "1pzsTNB9kYJA1oYTp2wfEcRVAGnJ3TBpZ",
  "owner_pubkey": "029e6605ed5cf4e76c810f2e6b439ad139c1b03b2ec47bbb325ddda8528ed7f36d",
  "p": "locus",
  "subtype": "AGENT",
  "ts": 1782340401,
  "type": "object",
  "v": 3
}
```

- Tip: vout 4 — 1 sat to `1pzsTNB9kYJA1oYTp2wfEcRVAGnJ3TBpZ` (matches `owner`).
- A correct implementation reproduces every derived field from `lat`, `lng`, `owner_pubkey`, `nonce` alone.

## 13. Reference implementation & tooling

- **Builder/parser (TypeScript):** `@locus/protocol` — `transaction-builder.ts` (`buildPlaceTx`, `buildMintTx`, `placeViaBrc100`, `buildJsonOpReturnScript`), `addressing.ts` (all derivations), `parse.ts` (`findLocusPayload`, `readScriptPushes` — the executable form of §5), `inscriptionLookup.ts` (chain-side resolution). To be published as `@locus/protocol` on npm.
- **MCP server:** `https://gryd.world/mcp` (Streamable HTTP, stateless, no auth) — the world and the payload builders as agent tools; signing always stays client-side. Tool list in `llms.txt`.
- **Reference indexer:** the-grid-api (Elixir) — `pin_chain_verifier.ex` + `pin_indexer.ex`, the executable form of §5/§10/§11. Public read API documented in `API.md` beside this file.

## 14. Security considerations

- **Metadata is attacker-controlled.** Treat as untrusted input everywhere (§6.7).
- **Spam is possible by construction** (§10.2). Clients filter views; indexers stay neutral.
- **No custody.** This document never requires revealing keys to any service. Signing is local; broadcast is anonymous.
- **Presence enforcement is centralized in this revision** (§8) — stated plainly so implementers know exactly which rule depends on the reference operator.

## Appendix A — region bounding boxes

First match wins; no match → `xx`. Coordinates are inclusive (`latMin ≤ lat ≤ latMax`, `lngMin ≤ lng ≤ lngMax`).

### Continents

| Code | Region | latMin | latMax | lngMin | lngMax |
|---|---|---|---|---|---|
| `aq` | Antarctica | -90 | -60 | -180 | 180 |
| `af` | Africa | -35 | 37 | -20 | 55 |
| `sa` | South America | -55 | 15 | -90 | -30 |
| `na` | North America | 5 | 85 | -170 | -30 |
| `eu` | Europe | 35 | 75 | -25 | 45 |
| `as` | Asia | -10 | 85 | 45 | 180 |
| `oc` | Oceania | -50 | 10 | 110 | 180 |

### Countries

| Code | Country | latMin | latMax | lngMin | lngMax |
|---|---|---|---|---|---|
| `us` | United States | 24.396308 | 49.384358 | -125 | -66.93457 |
| `ca` | Canada | 41.676555 | 83.113883 | -141 | -52.636291 |
| `mx` | Mexico | 14.532866 | 32.718655 | -118.455 | -86.703392 |
| `br` | Brazil | -33.74228 | 5.271841 | -73.99045 | -34.793147 |
| `ar` | Argentina | -55.05 | -21.78 | -73.56 | -53.63 |
| `gb` | United Kingdom | 49.9 | 60.9 | -8.65 | 1.77 |
| `fr` | France | 41.3 | 51.2 | -5.2 | 9.6 |
| `de` | Germany | 47.2 | 55.1 | 5.8 | 15.1 |
| `es` | Spain | 35.8 | 43.8 | -9.4 | 3.4 |
| `it` | Italy | 35.4 | 47.1 | 6.6 | 18.6 |
| `in` | India | 6.5 | 35.7 | 68.1 | 97.4 |
| `cn` | China | 18 | 53.6 | 73.4 | 135.1 |
| `jp` | Japan | 24 | 46 | 122 | 146 |
| `au` | Australia | -44 | -10 | 112 | 154 |
| `nz` | New Zealand | -47.5 | -34 | 166 | 179 |
| `za` | South Africa | -35 | -22 | 16 | 33 |

*(Coarse boxes by design — border precision is not consensus-critical; overlapping claims resolve to the first listed match.)*

## Appendix B — address hierarchy

`/128` continent · `/64` country · `/32` city · `/16` block · `/8` building · `/4` home · `/2` user · `/1` object.

---

*Published 2026-08-12. Documents the live mainnet format (v3) and the reference indexer. Nothing here requires chain changes — it publishes what is already true.*
