[← All NAPs](https://napplet.run/protocol/)

NAP-POW / PR #39

# NIP-13 Proof-of-Work Miner

Open PR

This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it.

Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/d85b4252f26fccd6c61bba18429ca9a6c0559210/naps/NAP-POW.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/39/files).

## NAP-POW

### NIP-13 Proof-of-Work Miner

`draft`

**NAP ID:** NAP-POW **Domain:** `pow` **Depends:**

-   `identity` — layering · required — the runtime stamps the user pubkey (identity domain) into every mined event
-   `relay` — capability · optional — a `mine` result may be published via `relay`
-   `outbox` — capability · optional — `mineAndPublish` uses outbox-aware fanout; same publish-consent policy as `outbox` **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.pow`; domain presence signals availability.

### Description

NAP-POW provides napplets with a shell-mediated [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof-of-work miner: the napplet hands over an event template and a target difficulty, the runtime mines a `nonce` until the event id carries the required number of leading zero bits, and returns the mined event. A napplet could grind nonces itself, but it runs in a single sandboxed context where a tight hashing loop blocks its own UI; the runtime can spread the search across **1..n web workers** (the web binding) or native threads (other bindings), report aggregate and per-worker progress, and arbitrate CPU across competing jobs. Difficulty mining is a long-running, cancellable, pausable operation, so NAP-POW models each request as a **job** the napplet can observe and control, not a single blocking call.

Mining commits the user's pubkey and `created_at` into the event id (NIP-13), so the runtime — which owns identity ([NAP-IDENTITY](https://github.com/napplet/naps/pull/12)) and signing — stamps those fields before the search begins; the napplet supplies only `kind`, `content`, and `tags`. Two entry points cover the common cases: `mine` returns the mined event to the napplet for it to use or publish later, and `mineAndPublish` mines, signs, and publishes the result through the shell's outbox-aware fanout in one step. As with all NAPs, napplets never receive signing keys; signing and publishing remain shell operations.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `mine` | `template` (`EventTemplate`), `target` (`uint`), optional `opts` (`PowOptions`) | `PowJob` handle | `pow.mine` plus push messages |
| `mineAndPublish` | `template` (`EventTemplate`), `target` (`uint`), optional `opts` (`PowOptions`) | `PowJob` handle | `pow.mineAndPublish` plus push messages |
| `queue` | none | list of `PowJobSummary` | `pow.queue` / `pow.queue.result` |
| `job` | `jobId` (`tstr`) | `PowProgress` | `pow.job` / `pow.job.result` |
| `hashrate` | none | `PowHashrate` | `pow.hashrate` / `pow.hashrate.result` |
| `cancel` | `jobId` (`tstr`) | `bool` | `pow.cancel` / `pow.cancel.result` |
| `pause` | optional `jobId` (`tstr`) | none | `pow.pause` / `pow.pause.result` |
| `resume` | optional `jobId` (`tstr`) | none | `pow.resume` / `pow.resume.result` |
| `formatHashRate` | `hashesPerSecond` (`number`) | `tstr` | local helper; no wire message |

`PowJob` is a local handle for `jobId` and `target`. It exposes `started`, `completed`, event listeners for `state`, `progress`, `done`, and `error`, and local `cancel`, `pause`, and `resume` helpers backed by the matching operations.

#### Schemas

Primitive references:

| Name | Meaning |
| --- | --- |
| `NostrEvent` | NIP-01 signed or unsigned event object. |
| `NostrTag` | NIP-01 event tag list. |

Enumerations:

| Name | Values |
| --- | --- |
| `PowState` | `queued`, `mining`, `paused`, `done`, `cancelled`, `error` |
| `PowJobSummary.mode` | `mine`, `mineAndPublish` |

`EventTemplate`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `uint` | yes | Event kind to mine. |
| `content` | `tstr` | yes | Event content to mine. |
| `tags` | list of `NostrTag` | no | Event tags to mine. |
| `created_at` | `uint` | no | Requested event timestamp. |

`PowOptions`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `workers` | `uint` | no | Requested worker count. |
| `priority` | `int` | no | Scheduling priority. |
| `timeoutMs` | `uint` | no | Mining timeout in milliseconds. |
| `commitCreatedAt` | `bool` | no | Whether the runtime should commit `created_at` at mining start. |

`PowStateChange`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | `tstr` | yes | Mining job identifier. |
| `state` | `PowState` | yes | New job state. |
| `position` | `uint` | no | Queue position when state is `queued`. |

`WorkerStat`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `workerId` | `uint` | yes | Worker identifier. |
| `bestPow` | `uint` | yes | Best leading-zero-bit count found by this worker. |
| `hashes` | `uint` | yes | Hashes attempted by this worker. |
| `hashRate` | `number` | yes | Worker hash rate in hashes per second. |

`PowProgress`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | `tstr` | yes | Mining job identifier. |
| `target` | `uint` | yes | Target leading-zero-bit count. |
| `state` | `PowState` | yes | Current job state. |
| `bestPow` | `uint` | yes | Best leading-zero-bit count found so far. |
| `bestNonce` | `tstr` | no | Nonce that produced `bestPow`. |
| `hashes` | `uint` | yes | Total hashes attempted for this job. |
| `hashRate` | `number` | yes | Job hash rate in hashes per second. |
| `workers` | list of `WorkerStat` | yes | Per-worker progress. |
| `elapsedMs` | `uint` | yes | Elapsed mining time in milliseconds. |

`PowWorkerHashrate`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `workerId` | `uint` | yes | Worker identifier. |
| `hashRate` | `number` | yes | Worker hash rate in hashes per second. |

`PowJobHashrate`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | `tstr` | yes | Mining job identifier. |
| `hashRate` | `number` | yes | Job hash rate in hashes per second. |

`PowHashrate`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `hashRate` | `number` | yes | Total hash rate in hashes per second. |
| `workers` | `uint` | yes | Active worker count. |
| `perWorker` | list of `PowWorkerHashrate` | yes | Per-worker hash rates. |
| `byJob` | list of `PowJobHashrate` | yes | Per-job hash rates. |

`PowResult`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | `tstr` | yes | Mining job identifier. |
| `ok` | `bool` | yes | Whether mining succeeded. |
| `event` | `NostrEvent` | yes | Mined event. |
| `pow` | `uint` | yes | Leading-zero-bit count achieved. |
| `nonce` | `tstr` | yes | Nonce value that satisfied the target. |
| `hashes` | `uint` | yes | Total hashes attempted. |
| `elapsedMs` | `uint` | yes | Elapsed mining time in milliseconds. |
| `published` | `PowPublishResult` | no | Publish outcome for `mineAndPublish`. |
| `error` | `tstr` | no | Error reason when mining or publishing failed. |

`PowPublishResult`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventId` | `tstr` | yes | Published event ID. |
| `relays` | map of relay URL `tstr` to `bool` | yes | Per-relay publish result. |

`PowJobSummary`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | `tstr` | yes | Mining job identifier. |
| `target` | `uint` | yes | Target leading-zero-bit count. |
| `state` | `PowState` | yes | Current job state. |
| `priority` | `int` | yes | Scheduling priority. |
| `position` | `uint` | no | Queue position when queued. |
| `bestPow` | `uint` | yes | Best leading-zero-bit count found so far. |
| `hashRate` | `number` | yes | Job hash rate in hashes per second. |
| `kind` | `uint` | yes | Event kind being mined. |
| `mode` | `mine` or `mineAndPublish` | yes | Job mode. |

`PowError`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | `tstr` | yes | Mining job identifier. |
| `error` | `tstr` | yes | Error reason. |

**`mine(template, target, opts?)`** — **Submits** a mining job and returns a `PowJob` handle immediately. The job is *enqueued*: depending on the shell's concurrency policy it begins mining at once or waits behind other jobs, reporting `state: "queued"` (with a queue `position`) until a worker slot frees. `job.started` resolves when mining actually begins; `job.completed` resolves with the mined **unsigned** event once `target` is met. The handle is valid the moment it is returned — `cancel`, `pause`, and queue inspection all work while the job is still queued.

When mining begins (not at submit time), the shell stamps the user pubkey and `created_at`, replaces any `nonce` tag with `["nonce", "<nonce>", "<target>"]`, and searches for a nonce whose event id has at least `target` leading zero bits, fanned across `opts.workers` workers (capped by shell policy). Stamping `created_at` at *mining start* — not enqueue — keeps a job that waited in the queue from committing a stale timestamp; `commitCreatedAt: true` (default) then freezes it for the whole search. `opts.timeoutMs` bounds **mining** time only and does not count queue wait. The mined event is returned unsigned; the napplet may publish it through [NAP-RELAY](https://github.com/napplet/naps/pull/2)/[NAP-OUTBOX](https://github.com/napplet/naps/pull/32), and the shell MUST sign the committed event **verbatim**, since altering `pubkey`, `created_at`, `tags`, `content`, or `kind` would invalidate the proof of work.

**`mineAndPublish(template, target, opts?)`** — Identical lifecycle and queue semantics to `mine` (submit → `queued` → `mining` → `done`), but on success the shell signs the mined event and publishes it using outbox-aware fanout (the user's write relays, plus recipient inbox relays for directed events). There is **no relay argument**: relay selection is shell policy, identical to [NAP-OUTBOX](https://github.com/napplet/naps/pull/32) `publish`. `PowResult.published` carries the fanout outcome; `job.completed` rejects with `"publish failed"`/`"publish denied"` if the proof of work succeeded but publishing did not.

**`queue()`** — Returns a summary of all of the napplet's tracked jobs (queued, mining, paused) including each job's `priority` and, for queued jobs, its `position`. For a miner/status napplet building a queue view. Ordering reflects the shell's current schedule.

**`job(jobId)`** — Returns a one-shot `PowProgress` snapshot for a single job: its `target`, `bestPow`, and per-worker `bestPow` / `hashRate`.

**`hashrate()`** — Returns the live miner-wide hash rate: the total, the per-worker breakdown, and the per-job breakdown. Rates are raw hashes per second; scaling to `kH/s`, `MH/s`, `GH/s` is a display concern — `formatHashRate` is local SDK sugar and sends no message.

**`cancel(jobId)`** — Stops a job and frees its workers. Resolves `true` if a job was running. The job's `completed` promise rejects with `"cancelled"`.

**`pause(jobId?)` / `resume(jobId?)`** — Suspend or continue a job, preserving its best-nonce-so-far and hash count. With `jobId` omitted, the whole miner pauses/resumes (e.g. to yield the CPU). A paused job reports `state: "paused"` and `hashRate: 0`.

### Wire Protocol

`pow.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`).

| Type | Direction | Payload fields |
| --- | --- | --- |
| `pow.mine` | napplet -> shell | `id`, `jobId`, `template`, `target`, `options?` |
| `pow.mine.result` | shell -> napplet | `id`, `jobId`, `accepted`, `state`, `position?`, `error?` |
| `pow.mineAndPublish` | napplet -> shell | `id`, `jobId`, `template`, `target`, `options?` |
| `pow.mineAndPublish.result` | shell -> napplet | `id`, `jobId`, `accepted`, `state`, `position?`, `error?` |
| `pow.state` | shell -> napplet | `jobId`, `state`, `position?` |
| `pow.progress` | shell -> napplet | `jobId`, `progress` |
| `pow.done` | shell -> napplet | `jobId`, `result` |
| `pow.error` | shell -> napplet | `jobId`, `error` |
| `pow.queue` | napplet -> shell | `id` |
| `pow.queue.result` | shell -> napplet | `id`, `jobs` |
| `pow.job` | napplet -> shell | `id`, `jobId` |
| `pow.job.result` | shell -> napplet | `id`, `progress`, `error?` |
| `pow.hashrate` | napplet -> shell | `id` |
| `pow.hashrate.result` | shell -> napplet | `id`, `hashrate` |
| `pow.cancel` | napplet -> shell | `id`, `jobId` |
| `pow.cancel.result` | shell -> napplet | `id`, `jobId`, `cancelled`, `error?` |
| `pow.pause` | napplet -> shell | `id`, `jobId?` |
| `pow.pause.result` | shell -> napplet | `id`, `jobId?`, `error?` |
| `pow.resume` | napplet -> shell | `id`, `jobId?` |
| `pow.resume.result` | shell -> napplet | `id`, `jobId?`, `error?` |

Key design notes:

-   The napplet generates `jobId` and uses it to correlate the shell's `pow.progress` / `pow.done` / `pow.error` push messages, exactly as NAP-OUTBOX uses `subId`. The shell rejects a duplicate `jobId` with `accepted: false`.
-   `pow.mine` / `pow.mineAndPublish` are acknowledged by their `.result`, which reports the job's **initial** `state` — `"queued"` (with `position`) if it is waiting, or `"mining"` if it started immediately. The mined event arrives later in `pow.done`, not in the ack.
-   `pow.state` is a shell push (no `id`) emitted on every lifecycle transition (`queued` → `mining` → `paused`/`done`/`cancelled`/`error`), carrying the new `state` and, while queued, the current `position`. It is how a napplet observes a queued job starting without polling; `job.started` resolves on the first transition to `"mining"`.
-   `created_at` is committed when the job **starts mining**, not when it is submitted, so queue wait never produces a stale timestamp. `options.timeoutMs` likewise measures mining time only.
-   `options.priority` is a scheduling hint (higher runs sooner); the shell MAY honor or ignore it. It never lets one napplet preempt another's jobs beyond shell policy.
-   `pow.progress` is a shell push with no `id`. The shell SHOULD throttle it (e.g. at most a few per second) regardless of hash rate.
-   `target` is the required leading-zero-bit count (NIP-13); `bestPow` / `pow` are achieved leading-zero bits of the event id.
-   `pause`/`resume` without `jobId` act on the whole miner.

#### Examples

**Mine a note to difficulty 21 with 4 workers — enqueued behind another job, then started and read back:**

```
-> {
     "type": "pow.mine",
     "id": "m1",
     "jobId": "job-1",
     "template": { "kind": 1, "content": "gm", "tags": [] },
     "target": 21,
     "options": { "workers": 4, "priority": 0 }
   }
<- { "type": "pow.mine.result", "id": "m1", "jobId": "job-1", "accepted": true, "state": "queued", "position": 1 }
<- { "type": "pow.state", "jobId": "job-1", "state": "mining" }
<- {
     "type": "pow.progress",
     "jobId": "job-1",
     "progress": {
       "jobId": "job-1", "target": 21, "state": "mining",
       "bestPow": 18, "bestNonce": "773201", "hashes": 2310000, "hashRate": 412000,
       "workers": [
         { "workerId": 0, "bestPow": 18, "hashes": 590000, "hashRate": 104000 },
         { "workerId": 1, "bestPow": 16, "hashes": 580000, "hashRate": 103000 },
         { "workerId": 2, "bestPow": 17, "hashes": 575000, "hashRate": 102500 },
         { "workerId": 3, "bestPow": 15, "hashes": 565000, "hashRate": 102500 }
       ],
       "elapsedMs": 5600
     }
   }
<- {
     "type": "pow.done",
     "jobId": "job-1",
     "result": {
       "jobId": "job-1", "ok": true, "pow": 21, "nonce": "1099148",
       "hashes": 4980000, "elapsedMs": 12100,
       "event": {
         "id": "000007abc…", "pubkey": "user…", "kind": 1, "content": "gm",
         "tags": [["nonce", "1099148", "21"]], "created_at": 1234567890
       }
     }
   }
```

**Mine and publish (outbox-aware, no relay argument):**

```
-> {
     "type": "pow.mineAndPublish",
     "id": "p1",
     "jobId": "job-2",
     "template": { "kind": 1, "content": "powered note", "tags": [] },
     "target": 24
   }
<- { "type": "pow.mineAndPublish.result", "id": "p1", "jobId": "job-2", "accepted": true, "state": "mining" }
<- {
     "type": "pow.done",
     "jobId": "job-2",
     "result": {
       "jobId": "job-2", "ok": true, "pow": 24, "nonce": "55230914",
       "hashes": 31200000, "elapsedMs": 74000,
       "event": { "id": "000000def…", "pubkey": "user…", "kind": 1, "content": "powered note", "tags": [["nonce", "55230914", "24"]], "created_at": 1234567999, "sig": "…" },
       "published": { "eventId": "000000def…", "relays": { "wss://relay.example.com": true } }
     }
   }
```

**Inspect the queue, then pause one job:**

```
-> { "type": "pow.queue", "id": "q1" }
<- { "type": "pow.queue.result", "id": "q1",
     "jobs": [
       { "jobId": "job-2", "target": 24, "state": "mining", "priority": 0, "bestPow": 22, "hashRate": 418000, "kind": 1, "mode": "mineAndPublish" },
       { "jobId": "job-3", "target": 28, "state": "queued", "priority": 5, "position": 0, "bestPow": 0, "hashRate": 0, "kind": 1, "mode": "mine" }
     ] }
-> { "type": "pow.pause", "id": "ps1", "jobId": "job-2" }
<- { "type": "pow.pause.result", "id": "ps1", "jobId": "job-2" }
```

**Live miner-wide hash rate:**

```
-> { "type": "pow.hashrate", "id": "h1" }
<- { "type": "pow.hashrate.result", "id": "h1",
     "hashrate": {
       "hashRate": 824000, "workers": 8,
       "perWorker": [ { "workerId": 0, "hashRate": 103000 } ],
       "byJob": [ { "jobId": "job-1", "hashRate": 412000 }, { "jobId": "job-2", "hashRate": 412000 } ]
     } }
```

#### Error Handling

Result and `pow.error` messages MAY include `error`. Common errors: `"duplicate jobId"`, `"invalid target"` (negative, or above the shell's cap), `"invalid template"`, `"timeout"` (target not met within `timeoutMs`), `"cancelled"`, `"unknown jobId"`, `"queue full"`, `"publish denied"` and `"publish failed"` (mineAndPublish only), and `"policy denied"`.

A timeout or cancel ends the job with `state: "error"` / `"cancelled"`, emits `pow.error`, and rejects the handle's `completed` promise. Best-effort progress emitted before the failure remains valid.

### Shell Behavior

-   The shell MAY queue submitted jobs and schedule them under its own concurrency policy; it MUST accept a job into the queue (or reject it with `accepted: false`) synchronously in the `.result`, reporting the initial `state` and, when `"queued"`, the `position`.
-   The shell MUST emit `pow.state` on every lifecycle transition so a queued job's start is observable without polling, and MUST stamp the committed `pubkey` (from user identity) and `created_at` at the moment mining begins — not at submit — then mine over that exact serialization; `commitCreatedAt: true` (default) freezes `created_at` for the whole search.
-   The shell SHOULD honor `options.priority` as an ordering hint where its policy allows, but MUST NOT let priority become a way for one napplet to starve or preempt another's jobs beyond policy.
-   The shell MUST insert or replace the `["nonce", "<nonce>", "<target>"]` tag per NIP-13 and MUST NOT return a result whose id has fewer than `target` leading zero bits.
-   The shell MUST treat `options.workers` as a hint and MAY clamp it to available cores and per-napplet policy.
-   The shell MUST respond to every request with a result carrying the same `id`, and MUST key every push (`pow.progress`/`pow.done`/`pow.error`) to the napplet-supplied `jobId`.
-   The shell MUST sign mined events only through its normal signing path; for a previously-`mine`d event submitted for publish, the shell MUST sign it verbatim without re-stamping committed fields, or reject it.
-   For `mineAndPublish`, the shell MUST publish using outbox-aware fanout (no napplet-supplied relays) and SHOULD apply the same publish consent policy as NAP-OUTBOX.
-   The shell SHOULD throttle `pow.progress` and SHOULD preserve best-nonce and hash counters across `pause`/`resume`.
-   The shell MAY queue jobs, cap concurrent jobs, total workers, and maximum `target`, and MAY enforce ACL checks on mine, publish, and queue inspection.

### Security Considerations

-   Mining is unbounded CPU work, and difficulty cost is exponential in `target`. The shell MUST cap maximum `target`, concurrent jobs, and total workers, and SHOULD enforce `timeoutMs`, so a napplet cannot pin the device or drain a battery by requesting an astronomically hard target or flooding the queue.
-   Signing and publishing remain shell-mediated; napplets never receive keys. `mine` returns an **unsigned** event — the proof of work is public and carries no secret — and signing happens only at publish time, gated by shell consent.
-   Proof of work commits `pubkey` and `created_at`. The shell MUST preserve every committed field when signing a previously-mined event; a shell that re-stamps `created_at` or reorders tags silently destroys the proof of work the napplet paid for.
-   Hash-rate and queue telemetry expose device capability and user activity. Shells MAY coarsen or rate-limit `pow.hashrate` / `pow.queue` for untrusted napplets.
-   A napplet could request mining of content it intends to attribute to the user. Because the id commits the user pubkey, `mineAndPublish` MUST run the same content/consent checks the shell applies to any signed publish.

### References

-   [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) — Proof of Work
-   [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md)

### Implementations

-   (none yet)

### Changelog

-   `f01fb31` - Introduced NAP-POW as a runtime-owned NIP-13 proof-of-work miner surface.
    
-   `fbedfa2` - Adopted injected-domain availability and linked the current upstream web binding.
    

[Join the discussion · PR #39 ↗](https://github.com/napplet/naps/pull/39)[Read proposed changes ↗](https://github.com/napplet/naps/pull/39/files)

On this pageSpecification metadata

Capability domain

`pow`

Repository state

Open PR

Registry status

Draft

Document marker

draft

Declared dependencies

-   [identity](https://napplet.run/protocol/nap-identity/)
-   [relay](https://napplet.run/protocol/nap-relay-pr-2/)
-   [outbox](https://napplet.run/protocol/nap-outbox-pr-32/)

Source revision

`d85b4252f26f`

[View source on GitHub ↗](https://github.com/napplet/naps/pull/39/files)
