NIPs by PolleramaCommunity NIPs, surfaced by trustConnect
npub1f27g79lrpey...

NIP-46 extension: batched NIP-44 decryption

Published Aug 10, 2026

NIP-46 extension: batched NIP-44 decryption

Motivation

Reading one gift-wrapped DM (NIP-59) costs two NIP-44 decryptions: the wrap (kind 1059) and the seal (kind 13). Under NIP-46 each decryption is a remote call costing two relay events, request and response.

Syncing N messages on a new device therefore costs 4N events between one signer/client pair, published in a burst. At 500 messages that is 2000 events, which trips relay rate limits: responses are dropped and the sync never completes.

The decryption count is fixed by the encryption scheme. The round-trip count is not.

Method: nip44_decrypt_batch

Request

{
  "id": "<random string>",
  "method": "nip44_decrypt_batch",
  "params": ["<json array of [peer_pubkey, ciphertext] pairs>"]
}

params holds exactly one element: a JSON-encoded array of two-element arrays. Each pair is [peer_pubkey, ciphertext], the two arguments nip44_decrypt takes. Keeping the batch in one string leaves params a flat array of strings, as in every other NIP-46 method.

Response

{
  "id": "<same as request>",
  "result": "<json array of plaintexts>"
}

result is a JSON-encoded array with the same length and order as the input. Element i is the plaintext for pair i, or null if that payload could not be decrypted (malformed payload, wrong conversation key, MAC failure). A backlog routinely contains undecryptable events; one must not fail the rest.

Errors

error is a free-form string in NIP-46, which gives a client nothing to branch on. This method constrains it: when a batch fails as a whole, error MUST begin with one of the tokens below, terminated by end-of-string or a colon. Text after the colon has no protocol meaning.

error := token [ ":" SP human-readable-text ]
TokenMeaningClient MUST
unsupported_methodSigner does not implement batching.Disable batching for the session; fall back to per-item.
invalid_paramsBatch was malformed.Surface as a client bug. MUST NOT disable batching.
batch_too_largeBatch exceeded the size cap.Re-split and retry. MUST NOT disable batching.
approval_requiredNo standing grant covers these items.Suspend batching, fall back per-item, re-arm per below.
rejectedUser denied the request.Fail the pending calls. MUST NOT disable batching.

unsupported_method is the only token that permanently disables batching. Treating invalid_params as unsupported makes a client abandon batching over its own serialization bug; treating rejected as retryable re-sends a denied batch on every attempt.

Undecryptable payloads are null entries, not errors. The batch fails whole only for the five reasons above.

Size limits

The cap is a byte budget, not an item count: the response carries every plaintext inside one kind:24133 event and must fit relay size caps, and items range from a few dozen bytes to ~2 KB.

  • Clients MUST bound a batch by the summed byte length of its ciphertexts, and SHOULD assume

32 KiB until told otherwise.

  • Signers MUST accept batches totalling up to 32 KiB.
  • A signer rejecting an oversized batch MUST answer batch_too_large: <bytes>, the largest

total it accepts, in decimal. Clients MUST adopt that value for the session, re-split and retry.

  • Signers MUST NOT truncate: result is positional, so a short array is indistinguishable

from one whose tail failed to decrypt.

Permissions

Batching grants nothing nip44_decrypt does not. A signer MUST NOT treat a batch as a separate permission scope, and MUST NOT decrypt an item it would refuse individually.

Signers SHOULD serve a batch only under an existing standing grant for nip44_decrypt, and answer approval_required otherwise. Prompting per item defeats the purpose; prompting once for fifty items asks the user to approve what they cannot inspect.

approval_required suspends batching, it does not end it. After falling back, a client MUST re-enable batching once an individual nip44_decrypt returns a plaintext — that approval is the condition the batch was waiting for. A client that treats the token as terminal stays degraded for the whole session after the grant has landed.

Discovery

NIP-46 has no capability negotiation, so support is probed. The probe must survive signers that answer an unknown method with silence instead of an error.

  • Signers MUST answer an unrecognised method with unsupported_method. Clients MUST NOT

rely on this.

  • A session's first batch MUST carry exactly one item. It is a decrypt the client needed

anyway, so a successful probe costs nothing extra.

  • Clients MUST bound the probe with a timeout, SHOULD use 10 seconds, and MUST treat expiry

as unsupported_method.

  • After a successful probe a client SHOULD batch for the rest of the session and MUST NOT

re-probe, subject to the re-arm and re-split rules above.

A capability list in NIP-46 would replace the probe.

Out of scope

nip04_decrypt and nip44_encrypt share the shape and could take identical treatment. NIP-04 is deprecated, and the observed rate-limit failures are on the decrypt side. Nothing here blocks adding them.

Reference implementation

Amber (Android), EventNotificationConsumer.handleNip44DecryptBatch, implements the wire format on the NIP-46 relay path: positional result, per-item null, whole-batch failure when an item needs approval. It does not yet emit the error tokens or the byte budget.