Self-contained proof
The hash, the bytes and the commitment are all in the same witness. Verifying the artifact requires the reveal transaction and nothing else.
Application layer · marker drops
A Drops artifact is a media object small enough to sit inside its own proof. There is no separate content server to trust, no envelope to reassemble, and no chunking convention to get wrong. What the leaf holds is what the artifact is.
Most artifact formats on Bitcoin optimise for size: how much can be pushed on chain, in how few transactions. Drops optimises for the opposite property. A body that fits in 256 bytes is a body that can be shown in full next to its own hash, reproduced from a single witness, and reasoned about without streaming.
The hash, the bytes and the commitment are all in the same witness. Verifying the artifact requires the reveal transaction and nothing else.
No chunk ordering, no continuation markers, no partial-artifact state. An artifact is present or absent.
Larger material is referenced by a content-addressed pointer written into the body. The reference is visible, so its trust assumptions are visible too.
| Content type | What fits |
|---|---|
text/plain | Roughly 256 ASCII characters, or fewer for multi-byte UTF-8. A statement, a name, a key fingerprint, a short poem. |
image/svg+xml | A compact vector mark. Geometry, not illustration. Note the serving policy below. |
application/json | A small structured record. The Pacts reference profile is one such body, and lands around 205 bytes at its longest. |
application/vnd.drops.pact-seed | The fixed 184-byte Pact Seed record. |
| A content-addressed pointer | A URI naming a hash, for example an IPFS CID or a bare SHA-256 with a retrieval hint. The pointer is on chain; the payload is not. |
When a body holds a reference rather than the content, Bitcoin proves the reference, not the thing referenced. That is still useful: it fixes which bytes were meant. It is not the same as the content being on chain, and an interface should not present the two as equivalent.
The protocol does not maintain an allowlist. Any string matching the restricted-name grammar is a valid content type in a leaf. That is a deliberate separation: what may be recorded is broad, and what may be rendered is narrow.
Text/Plain is invalid.text/plain; charset=utf-8 is invalid; the charset belongs inside the body's own convention.^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$A conforming implementation that serves artifact bodies over HTTP must assume the body is hostile, because anyone can put anything in one. The reference body endpoint applies a passive-content policy: a small set of types keep their declared media type and render inline, and everything else is served as an opaque download.
| Declared content type | Served as | Why |
|---|---|---|
text/plain | Inline, with its declared type | Passive. Cannot execute, cannot fetch, cannot navigate. |
image/png, image/jpeg, image/gif, image/webp, image/avif | Inline, with its declared type | |
image/svg+xml | application/octet-stream attachment | SVG is an active document format. It can carry script and external references. |
text/html, application/javascript, application/xml, application/json, application/pdf | application/octet-stream attachment | Active, script-bearing, or capable of external fetches. |
| Anything else, including unknown types | application/octet-stream attachment | Default deny. An unrecognised type is not a safe type. |
The same response carries the headers that make caching safe and rendering contained:
ETag: "<sha256 of the body, quoted>"
Cache-Control: public, max-age=31536000, immutable
Content-Security-Policy: default-src 'none'; sandbox
X-Content-Type-Options: nosniff
Content-Disposition: inline | attachment, per the table above
A year-long immutable cache is correct here for the same reason the artifact exists: the bytes at a given Drop identity can never change. The ETag is the body hash, so a cache validator and a protocol proof are the same value.
An artifact has one creation event and any number of custody moves. It has no update, no revision, and no delete.
A Taproot output commits to a script tree containing the Drops leaf. Nothing is public yet. An observer sees an ordinary P2TR output.
The output is spent on the script path. The leaf and the control block enter the witness. The artifact now exists on chain but is not yet recorded.
At the implementation's confirmation depth, and only after the commitment verifies, the indexer records the artifact. Its identity is fixed at this point and never changes.
Custody sits at output 0 of the reveal transaction. Custody status is active.
Spending the custody outpoint moves custody to output 0 of the spending transaction, provided that output is a P2WPKH or P2TR output with a positive value. The transfer count increments.
If a spend does not produce a supported output 0, custody status becomes burned. The record remains readable forever; only its custody chain stops.
The full custody rules, including the exact script patterns and the legacy_unresolved status, are normative in specification section 6.2.
These are the fields the reference indexer stores and returns for an artifact. Field names match the API reference.
| Field | Type | Notes |
|---|---|---|
dropId | string | The portable identity. The only field safe to key on. |
marker | drops or drops-pact | From the leaf. |
mime | string | The declared content type, verbatim. |
bodyBytes | integer, 1 to 256 | Body length. |
bodySha256 | 64 lowercase hex | Equals the leaf's hash field, and equals SHA-256 of the served body. |
dropmark | 10 uppercase hex | Decorative short label. Not an identity. |
creatorPubkey | 64 lowercase hex | The x-only key from the leaf. |
initialOwnerAddress | string or null | Address of output 0 of the reveal transaction. |
currentOwnerAddress | string or null | Address holding the current custody outpoint. |
custodyStatus | active, burned, legacy_unresolved | See specification 6.2. |
currentCustodyOutpoint | object or null | txid, vout, scriptHex, valueSats. |
custody | object | Profile drops-custody-v1, plus transferCount, lastTransferHeight, lastTransferTxid, and the projection's verification height and block hash. |
reveal | object | txid, input, height, blockHash. |
sequence, displayName | integer, string | Presentation only. Deployment-local. |
pactSeed, pactsReference | object | Present when the body matched one of the two Pact body profiles. |
Everything above except sequence, displayName and the custody verification metadata is derivable from chain data alone. That is the test for whether a field belongs in a protocol record: could a second implementation compute it and get the same answer?