> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/sources/zapstore.md` · snapshot 2026-10-10

# Source: Zapstore + NIP-82 (Software Applications)

> Reference layer for the protocol Zapstore implements. Primary implementation:
> [`github.com/zapstore`](https://github.com/zapstore). The spec is being
> codified as NIP-82 ([PR nostr-protocol/nips#1336](https://github.com/nostr-protocol/nips/pull/1336),
> in flight at the time of writing — 2026-05-12). Always cross-check the
> live `zsp` source if a tag or field disagrees with this summary.

## The model

Zapstore is **not a server**. It's a convention over three things every Nostr
client already has: addressable events, file blobs, and outbox-style relay
routing. A "published app" is:

- a **kind 32267** event (app metadata, replaceable by package id) signed by
  the publisher,
- one or more **kind 30063** events (one per release version, replaceable per
  `package_id@version`), each pointing at…
- one or more **kind 3063** events (per-binary metadata: hash, URL, size,
  cert), whose `url` tags resolve to **Blossom** blobs.

Discovery is just an `nostr+REQ` for `kind: 32267` with a tag filter. The
Zapstore app and `zapstore.dev` are *one client* over that data; anyone can
build another.

## Event kinds

### kind 32267 — Software Application (addressable, `d` = package id)

Required tags: `d` (package id, e.g. `com.example.app`), `name`, `icon` (URL).
Common tags emitted by `zsp` v0.4.10:

| Tag | Meaning |
|---|---|
| `d` | package id (e.g. `com.otsuite.lite.debug`) |
| `name`, `summary`, `icon` | display fields |
| `t` | topic/category (repeatable) |
| `repository` | URL or NIP-34 `naddr` to the source repo |
| `a` | full NIP-34 coordinate `30617:<pubkey>:<d>` for the repo announcement |
| `f` | platform/arch filter (`android-arm64-v8a`, `android-x86_64`, …) |
| `h` | platform-grouped hash (used by clients to detect changes across releases) |

`content` is the long description (markdown OK).

### kind 30063 — Software Release (addressable, `d` = `package_id@version`)

A *set* event in the NIP-51 sense. Links one or more kind-3063 assets that
together make up a release.

| Tag | Meaning |
|---|---|
| `d` | `<package_id>@<version>` |
| `i` | package id (so clients can filter releases for a given app) |
| `version` | semver string |
| `c` | channel (`main`, `beta`, `nightly`, `dev`) |
| `f` | arch filter (repeated per arch the release supports) |
| `e` | event id of each kind-3063 asset (with relay hint) |

### kind 30509 — NIP-C1 Cryptographic Identity Link (addressable, `d` = cert SHA-256)

Used by `zsp identity --link-key` to bind an APK signing certificate to a
Nostr identity. Clients that walk the kind 32267 → 30063 → 3063 chain compare
the 3063's `apk_certificate_hash` to the latest 30509 `d`-tag under the same
publisher pubkey — a mismatch signals a possible repo hijack.

| Tag | Meaning |
|---|---|
| `d` | SHA-256 of the certificate (DER form) |
| `signature` | RSA (or ECDSA, depending on the key) signature over the publisher's npub, signed by the APK signing key. The cryptographic proof of co-control. |
| `expiry` | Unix timestamp after which the proof is no longer considered valid. Default 1 year; tunable via `zsp identity --link-key-expiry`. |

### kind 3063 — Software Asset (regular, one per binary)

This is the **file-metadata** event. It is `zsp`'s own kind — *not* NIP-94's
kind 1063. The NIP-82 PR draft discussed 1063; the shipped `zsp` v0.4.10 uses
3063. If you only have the spec PR open, you'll be misled — check the wire.

| Tag | Meaning |
|---|---|
| `i` | package id |
| `x` | SHA-256 of the asset bytes (the Blossom address) |
| `url` | Blossom URL (`<server>/<sha256>`) |
| `m` | MIME (e.g. `application/vnd.android.package-archive`) |
| `size` | bytes |
| `f` | arch filter (one tag per arch in the binary; fat APKs get multiple) |
| `min_platform_version` / `target_platform_version` | Android min/target SDK |
| `filename`, `version_code`, `version` | display + ordering metadata |
| `apk_certificate_hash` | SHA-256 of the APK signing cert (used by NIP-C1) |

## Blossom usage

- Default upload target: `https://cdn.zapstore.dev`.
- Override: `BLOSSOM_URL=<server-base-url>` env var.
- The CLI uploads icons, screenshots, and the APK itself — anything the events
  reference by `url`. Blobs are content-addressed, so re-uploading an
  unchanged asset is a no-op.

## Relay usage

- Default publication target: `wss://relay.zapstore.dev`.
- Override: `RELAY_URLS=<comma-separated-list>` env var.
- `relay.zapstore.dev` enforces a **whitelist** on first-time publisher
  pubkeys. Either (a) commit a `zapstore.yaml` with the publisher pubkey at
  the root of the linked source repo so the relay can verify pubkey↔repo, or
  (b) accumulate prior reputation via their Vertex system.

## Signing

`zsp` reads the signer from `SIGN_WITH`. Supported forms:

| Value | Meaning |
|---|---|
| `nsec1…` | Raw nsec |
| `<hex>` | Raw hex private key |
| `bunker://<pubkey>?relay=…&secret=…` | NIP-46 remote signer |
| `browser` | NIP-07 (opens a localhost preview page that signs via your browser extension) |
| `npub1…` | Use the given identity for *preview only*; pairs with `--offline` |

## Cert linking — NIP-C1

`zsp identity --link-key key.p12` (or the interactive prompt on first
publish) produces a NIP-C1 proof that ties the **APK signing certificate** to
the **Nostr identity**. Clients use it to detect repository hijacks: a new
release signed by a different cert under the same npub flags as suspicious.

Skip with `--skip-certificate-linking` when:
- The cert is a debug keystore (the default Android debug cert is shared
  across many machines and effectively meaningless as a proof).
- You're producing a sandbox publish that won't be consumed by clients
  checking provenance.

For any release you intend to ship to the public Zapstore, **do** the linking
with the actual release keystore.

**Format note.** `zsp identity --link-key` accepts `.p12`/`.pfx`/`.pem`/`.crt`
directly. **`.jks` (Java KeyStore) needs a one-shot conversion first** —
`zsp` will error with a `keytool -importkeystore` command if you point it at
a JKS. Sample:

```bash
keytool -importkeystore -noprompt \
  -srckeystore release.jks   -srcstoretype JKS    -srcstorepass "$PW" -alias <key-alias> \
  -destkeystore release.p12  -deststoretype PKCS12 -deststorepass "$PW"
```

## CLI: `zsp`

```bash
go install github.com/zapstore/zsp@latest      # binary lands in ~/go/bin/
```

Modes:

| Invocation | Use |
|---|---|
| `zsp publish --wizard` | Interactive setup. Writes a `zapstore.yaml` for you. |
| `zsp publish zapstore.yaml` | Config-driven. The form to script against. |
| `zsp publish app.apk -r github.com/u/r` | Local APK + forge URL. `-r` does not accept `naddr`; use the yaml. |
| `zsp publish ... --offline` | Sign events without uploading or publishing. Events to stdout, upload manifest to stderr. **The standard dry-run.** |
| `zsp utils extract-apk app.apk` | Read-only: dump package id / version / cert / icon. |
| `zsp utils check-releases <src>` | Detect whether upstream has a newer version than what's published. |

## Links

- [`zapstore/zsp` README](https://github.com/zapstore/zsp) — canonical CLI
- [Zapstore publishing docs](https://zapstore.dev/docs/publish)
- [Zapstore FAQ](https://zapstore.dev/docs/faq)
- [NIP-82 PR (nostr-protocol/nips#1336)](https://github.com/nostr-protocol/nips/pull/1336)
