What this is. A companion to BRC-515 holding two things: diagrams of the mechanism the spec describes in prose, and the method and results of checking its arithmetic independently. The spec says what the protocol is; this shows how it behaves.
Section numbers in the form §4.3 refer to BRC-515. Other BRCs are cited by number.
Fourteen review findings have been applied to the spec and are recorded below for provenance. Three items remain, and every one of them lives outside this document.
Status
Remaining — four items, one resolved, none of them in BRC-515
| # | item | blocks | recommendation |
|---|---|---|---|
| R1 | MAP published two conflicting specs | resolved | Merged. opldotdev/MAP master now carries the six-command set. BRC-515's citation is no longer ahead of the spec it cites. |
| R2 | BRC-503 is unmerged | blocking | Land opldotdev/BRCs PR #1. Until it does, BRC-515's primary citation has no number. |
| R3 | MAP state along a 1Sat chain was unwritten | in review | Drafted as BRC-514 (PR #4, companion). §5 cites it; the interim rule is gone. |
| R4 | go-templates MAP parser | merged | Command-set fixes merged in both repos. ::: remains, tracked in 1sat-sdk#28. |
Applied to BRC-515
| was | now in the spec |
|---|---|
| §5 re-implemented BRC-159's origin, transfer, tip and pairing, with an incompatible pairing rule | §5 splits ownership (the asset's own model — 1Sat spend authority, an AIP or Sigma signature, or whatever a system defines) from position (this document's). Pairing table, Order and Tip removed. |
“Only the holder can move it” was prose, with nothing stopping a stranger's SELECT from unplacing an object | An indexer MUST ignore a position command it cannot authorize, and MUST state which model it enforces. See Where ownership comes from. |
“The quadkey in effect at the tip” was defined only in a non-normative note | Moved out entirely. §5 now cites BRC-514, which owns the revision order, the state model and every command's fold. |
| Placement assumed a 1Sat token throughout | A placed object is any output whose resolved MAP state carries a quadkey. Spend authority is one binding, not the definition. |
| §1 and §5 disagreed about what unplaces an object | One rule: removing quadkey unplaces. Transfers relocate nothing on their own. |
DELETE used for a scalar key; no way to express multi-cell placement | §2.1 maps the four MAP commands onto place / add a cell / drop a cell / unplace. quadkey is list-valued. |
bare lat / lng forked MAP's namespace; no altitude | coordinates.lat / .lng / .alt, single-valued. Bare lat/lng forbidden. |
myapp:<seed> recommended; a bare seed silently merged worlds | Genesis outpoint preferred, hash second, myapp: a fallback. Bare seeds forbidden. app has no role in world identity. |
silent on whether a move may change world | Portals specified: world may change, identity survives, one world at a time, arrival is not authorization. |
| multiple MAP commands per output undefined | Cites BRC-503 §3's ::: separator. |
empty world both forbidden and required to be accepted | One clause with the write/read asymmetry stated. |
| numbered 229, below every dependency | BRC-515 — above the 500–513 corpus and clear of 314. |
| “placed output” had no 1-satoshi requirement, implying a second lineage model | Scope settled: an authorization model must be in force, and the indexer must say which one it enforces. 1Sat is one option. |
geohash aspect ambiguous; h3 unqualified; join tags duplicated MAP's list | Wording fixed; h3 is a cell index; join tags cite BRC-503 §5 instead of restating. |
Where BRC-515 sits
BRC-515 is a key profile. It names attributes that ride on an existing attribute protocol, attached to an object whose identity and movement come from a different protocol again. Drawing that layering out shows where each part of the current draft belongs.
BRC-515's own contribution
The quadkey grammar and cell math (§4), the key semantics and the
placing / non-placing distinction (§2), the world partition (§3), the tradeoff analysis against geohash and H3
(§4.9), and the prefix / viewport predicates including the ancestor rule (§6). None of this exists elsewhere.
Borrowed, and better left borrowed
Origin, transfer, tip and burn belong to BRC-159 and are now cited there, not
restated. The command set and the ::: framing are BRC-503's. The coordinates.* convention is
BRC-503 §5's. State resolution moved to BRC-514. An earlier
draft restated all of these, and in three cases restated them differently — which is how the pairing rule
came to contradict sat ordering.
Two layers: who may move it, and where it is
A location system sitting on top of an ownership system can easily collapse the two. §5 keeps them apart. Ownership answers who may move a thing. Position answers where it is and what a query returns. Only the second belongs to this BRC.
The diagram below shows the case where ownership comes from 1Sat: the object changes hands at every hop, and moves at only two of them. An indexer that reads a change of holder as a change of place has merged the layers.
Where ownership comes from
The spec requires an authorization model and mandates none, naming two that already exist. That is deliberate: the asset decides. A 1Sat token is owned by whoever can spend it. A system with no tokens at all might identify users by AIP or Sigma signature and treat the signer as the owner. Either works, and anything else a system defines works too. What an indexer may not do is skip the check.
| model | a command is authorized when |
|---|---|
| Spend authority (1Sat) | the transaction carrying it spends the object's current tip — so it is a link in that object's own chain |
| Identity signature | the AIP or Sigma signature on the carrying transaction belongs to the party entitled to edit the target record |
SELECT <txid> lets any transaction name any
earlier one — that is what the command is for — and MAP has no idea who owns anything. §2.1 used exactly that
grammar to unplace an object and never said which transaction was allowed to send it. Read literally, an indexer built to the
spec would accept SELECT <someone-else's-txid> REMOVE quadkey from a stranger and drop their object off the
map. An indexer built by the people who wrote it would not, because they knew what was meant. That gap is now an indexer
MUST.The fix stayed out of the ownership business. §5 requires that a model be in force, requires the indexer to say which one it enforces, and requires it to ignore commands it cannot authorize. Which model, and how it decides, is the asset's business.
The cell encoding, drawn
§4 specifies the quadkey in formulas and pseudocode. The formulas are correct (see Verification); they are just hard to hold in the head. These three figures are the same rules as pictures.
Digits are quadrants
One digit per zoom level, digit = x bit + 2 × y bit, with y growing south as in web tiles.
The consequence worth internalising: 0 and 1 are the northern half, 2 and 3 the southern.
That single fact is what makes the polar-cap rule below decodable on sight.
A prefix is an ancestor — which is the whole argument for quadkeys
Appending a digit descends a level; dropping one is the parent. So a region query is a string prefix match on any
ordinary key/value index, with no spatial index and no geometry library. That property, plus decoding straight to the
z/x/y pyramid every renderer already draws, is what §4.9 spends its comparison grid buying — and what it pays
for at the poles.
z=18 New York vector. Each prefix decoded with the §4.5
inverse and checked to contain the point — nesting confirmed empirically, not assumed.One object, many cells
§2 makes quadkey list-valued, using MAP's own commands: SET places, ADD adds a
cell, DELETE drops one, REMOVE clears them all. For anything linear or irregular — a road, a
river, a border, a delivery zone — that removes a bad trade. The smallest single cell containing a road is enormous and
mostly empty; minting separate objects loses the shared identity.
The projection stops before the poles
Web Mercator maps the ellipsoid to a square that cuts off at ±85.0511287798°. §4.1 spells the consequence out, and the rule has a pleasant property: because the north cap collapses into tile row
y = 0 and the south into y = n−1, a polar record is visible in its quadkey alone — all-{0,1}
digits means north cap, all-{2,3} means south. Longitude still resolves inside the cap; latitude does not.
z=3 for legibility; cap bands exaggerated — the real clip discards 4.95° of latitude
at each pole. This is why §4.1 says polar content SHOULD carry coordinates.lat / coordinates.lng.Verification — method and results
§4 is the part of BRC-515 that can be settled by computation, so it was. The method: implement §4.1–4.5 from the specification text alone, without consulting an existing quadkey library, then compare against every numeric claim the document makes. An independent implementation that reproduces the published vectors shows the prose is unambiguous enough to implement.
| claim | source | result |
|---|---|---|
MAX_LAT value | §4.1 | reproduced 85.0511287798066 |
| Three test vectors | §4.8 | reproduced 032010110301120232, 0320101103011202, 3 — exact |
Bounding box of the z=18 vector | §4.8 | reproduced matches the published figures to six decimals |
| Zoom → equator edge table, all 7 rows | §4.7 | reproduced against 40075016.69 / 2^z |
| Polar cap digit restriction | §4.1 | confirmed points at ±89° clamp to y=0 / y=n−1, longitude still resolving |
| Exact prefix nesting | §4.6 | confirmed the NY point falls inside all six prefixes checked |
The y formula | §4.2 | equivalent the standard slippy-map formula in artanh form — algebraically identical, better conditioned near the equator |
(0, 0) vector at
z=1 correctly yields 3 rather than 0. Implementations that omit it will pass the New York vectors
and fail on the equator and prime meridian. And the artanh form of the y formula is a deliberate improvement
over the more commonly published tan/sec version, not a transcription error.The prefix decode — every row computed, every row containing the point:
| prefix | z | tile (x, y) | bbox lon | bbox lat | equator edge |
|---|---|---|---|---|---|
| 0 | 1 | 0, 0 | −180 … 0 | 0 … 85.0511 | 20 038 km |
| 03 | 2 | 1, 1 | −90 … 0 | 0 … 66.5133 | 10 019 km |
| 032 | 3 | 2, 3 | −90 … −45 | 0 … 40.9799 | 5 009 km |
| 0320 | 4 | 4, 6 | −90 … −67.5 | 21.9430 … 40.9799 | 2 505 km |
| 0320101103011202 | 16 | 19288, 24645 | −74.0479 … −74.0424 | 40.6890 … 40.6931 | 611 m |
| 032010110301120232 | 18 | 77154, 98583 | −74.0451 … −74.0437 | 40.6890 … 40.6900 | 153 m |
Worlds, and what makes an id
§3 partitions the cell space: the same cell in two worlds is not the same place, and Earth is expressed by omitting the key entirely so the common case costs zero bytes. The part that needs care is what a non-Earth id may be.
world bytes. It never consults MAP's app key — §2 says indexers
MUST NOT even require app — so the id has to carry its own uniqueness.myapp:<seed> duplicates MAP's
app key, yet dropping the prefix is unsafe: with world equality defined on world alone, a bare
world=seed-7 silently merges every unrelated app shipping the same string. §3 resolves it by leading with a
genesis outpoint, offering a hex hash for off-chain worlds, demoting myapp:<seed> to a fallback, and
forbidding bare seeds outright.Portals
§5 allows world to change on any authorized write. Because MAP state carries forward, a write that sets only world leaves quadkey in effect — the object arrives at the same cell in a different world. The object's identity is untouched — under spend authority, the BRC-159 origin — so identity survives the trip.
REMOVE world rather than
overwriting it. SET world earth is honoured under §3's tolerance rule but SHOULD NOT be written in
preference. An object holds many cells and exactly one world.What a region query returns
§6 defines queries as predicates, which is the right altitude. The rule an implementer will skip is the third one: a viewport matches not only the cells inside it and the cells covering it, but its ancestors — otherwise a city-sized tag silently disappears the moment the user zooms to a street.
What remains
Three open, one closed. None is a change to BRC-515 — each lives in a neighbouring repository or an unwritten document.
closedresolved R1 — MAP's two specs are now one
MAP carried two incompatible specifications on two branches, and the default branch was the older one:
| branch | commands | what DELETE meant |
|---|---|---|
| master (was default) | SET, DELETE | Removed a key/value mapping — the scalar removal command. |
| v2 | all six | Removes values from a list. REMOVE takes the scalar role. |
That disagreement is where the downstream confusion came from: go-templates and 1sat-sdk both
shipped a DEL command that exists in neither spec, and an earlier draft of this BRC used DELETE for
a scalar key — correct under master, wrong under v2.
opldotdev/MAP PR #3 is merged; master now defines
SET, REMOVE, ADD, DELETE, SELECT and CLEAR,
with DELETE taking a key and then the values to strike. BRC-515 no longer cites a spec that contradicts its
own default branch. The v2 rewrite had dropped the author credits master carried, so those were restored before the merge
rather than lost to it.blocking R2 — BRC-503 is not merged
BRC-515's primary citation is BRC-503, which exists only in opldotdev/BRCs
PR #1 alongside the rest of the 500–513 corpus. Until that lands, the MAP dependency has no number, which is the
original reason the draft restated MAP's command set in the first place. It is also why 515 was chosen: it clears the whole
corpus and the contested 314. Land PR #1 first or alongside.
highin review R3 — MAP state along a 1Sat chain now has a spec
“The quadkey in effect at the tip” is only well defined if there is a rule for resolving a MAP key's
value across a chain of outputs. MAP cannot supply it — it has no ownership model, and its only context mechanism
(SELECT <txid>) addresses a transaction. BRC-160 declines it explicitly: it does not say which envelope is “the” content for an application — those rules “belong in application or content-resolution specs”.
Implementations resolve it anyway, and have for years: merge each MAP revision along the chain in sequence order, last
write wins. 1sat-stack indexes rev:, map: and par: per origin per sequence
and merges forward.
REMOVE needs a SELECT at all — and leaves it open for a reviewer (Q4 on the
514 companion) rather than settling it. BRC-515 §5 now cites it rather than carrying an interim rule. Open until PR #4 merges.highmerged R4 — two implementations carried the same MAP parser defect
Verified against current code in both repositories, not module caches.
| repo | file | state |
|---|---|---|
| go-templates | template/bitcom/map.go | Declares SET, DEL, ADD, SELECT. Implements SET only. |
| 1sat-sdk | packages/templates/src/bitcom/map.ts | Same four in a MAPCommand enum — a direct port, same DEL. |
Neither DEL nor the missing REMOVE/CLEAR matches either MAP branch. The shared
defects:
| defect | detail |
|---|---|
DEL is not a MAP command | It appears in no MAP branch and in no other implementation. go-map uses "DELETE"/"REMOVE"; bmap switches on both. |
| Dead constants | In go-templates, MapCmdDel, MapCmdAdd and MapCmdSelect are referenced by nothing — not even tests. Only SET reaches a code path. |
| Silent empty decode | A non-SET command returns a non-nil result with an empty data map. Callers treating non-nil as “decoded” lose data without an error. |
No ::: handling | Zero occurrences in go-templates. A ::: push is swallowed as a key or a value, so a second command in one output corrupts the first. |
| No list values | Adds []string (Go) and adds?: string[] (TS) are declared and never populated. Multi-cell placement cannot be parsed at all. |
| Wrong protocol name | 1sat-sdk map.ts says “MAP (Metadata and Payload) Protocol”. It is the Magic Attribute Protocol — the SDK's own constants.ts has it right. |
DEL — nothing on chain was ever written as it, so there was no migration.
go-templates#4 also implemented the missing decoders and
made an unrecognised command return nil rather than a silently empty record;
1sat-sdk#26 fixed the writers, which were building DELETE
output that could never be decoded back. The ::: separator is deliberately still unhandled and is tracked in
1sat-sdk#28.Also noticed, not a defect: 1sat-sdk's
BSocialContext already uses geohash as a context key. BRC-515 declares geohash as a
non-placing read key derived from the same point, so the two uses are compatible — worth confirming the value grammar
matches when both land.