diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15f3eafd7..758cf8758 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ Crypto:
SMP protocol:
- `ROWN` command (v23): the names an address owns, and whether the account has
been used at all, so a recovered wallet seed can be scanned for the accounts
- already in use.
+ already in use. See `plans/2026-09-22-names-owned-by.md`.
# 6.5.1
diff --git a/plans/2026-09-22-names-owned-by.md b/plans/2026-09-22-names-owned-by.md
new file mode 100644
index 000000000..92a939d4b
--- /dev/null
+++ b/plans/2026-09-22-names-owned-by.md
@@ -0,0 +1,78 @@
+# Owned-by: the names an address owns
+
+A recovered seed does not say how many of its accounts have been used. Until it
+does, a restored device cannot take a fresh account without risking one its
+owner already has, and cannot find the names it already holds. This adds the
+question the protocol could not ask: what does this address own.
+
+This is Workstream B of the SimpleX names v2 plan, after the Ethereum crypto
+primitives. The client consumer is the wallet recovery scan in simplex-chat.
+
+## What this delivers
+
+```
+GET /v2/owned-by/
?offset=N resolver, JSON OwnedNames
+ROWN -> ROWND SMP v23 (nameOwnedSMPVersion)
+ownedNames server, agent client, agent
+```
+
+The agent returns the relay it used alongside the answer, so a caller can pass
+it back as used and ask the next account elsewhere. Sending every account of a
+seed to one relay would tell that relay the accounts belong to one wallet; the
+scan therefore asks one account per relay, and `getNextNameServer` avoids the
+hosts already used where the configured set allows. `proxiedSMPRelayVersion` is
+pinned to the current version so ROWN can be proxied at all — a scan that falls
+back to a direct session hands the relay its address with its IP.
+
+Enumeration is read off the ERC-721 registrar (`balanceOf`,
+`tokenOfOwnerByIndex`, `labelOf`), not from registration logs: the token is the
+name, so a name acquired by transfer counts. The registrar does not maintain
+enumeration on expiry — it states this as an invariant, and burns a token only
+when the name is re-registered — so a lapsed name stays listed. It is reported
+with its `status` rather than filtered, because a caller scanning a recovered
+seed is exactly the caller who needs to be told a name can still be renewed.
+
+## What `inUse` actually answers
+
+`inUse` is `holds a name now, or nonce > 0, or balance > 0`, on the one chain
+the resolver is configured with. Holding a name is only one way for an account
+to be in use: an account that was funded, or ever sent a transaction, is in use
+with no name, and a scan that reads names alone hands out an account its owner
+is already using. Within that scope the signal is sound — an EOA's balance can
+only fall through its own transaction, which bumps the nonce, so "funded then
+drained" still reads as used, and an EIP-7702 delegation leaves the authority's
+nonce incremented, so it needs no separate code check.
+
+It does not see:
+
+- **Accounts holding only other tokens.** An EOA can hold ERC-20s or NFTs with
+ nonce 0 and no ether. ERC-20 has no reverse index, so answering this needs
+ `eth_getLogs` over Transfer topics for all history, which providers cap, or an
+ external indexer. Out of scope here.
+- **Accounts used on another chain.** The same key is the same address on every
+ L2 and sidechain; this chain's nonce and balance stay 0.
+- **History, as opposed to current state.** `balanceOf` is present ownership. A
+ name registered for an account by a controller — which leaves the owner at
+ nonce 0 — and later transferred away, or lapsed and re-registered by someone
+ else, leaves no trace.
+
+The cost is bounded. A name-holding account is always found, so no account that
+holds a name is ever mislabelled. A missed account matters only as a gap-limit
+reset: the scan can stop up to `scanGapLimit` indexes early and not reach a name
+beyond that. That is recovery incompleteness, recoverable by scanning again on a
+better signal, not loss. In the other direction a dust transfer marks an account
+used; the indexes come from hardened derivation with no published xpub, so they
+cannot be enumerated to be dusted.
+
+## What is deliberately absent
+
+- **No token or indexer integration**, per the limits above.
+- **Nonce and balance are not carried over SMP.** The resolver reports them, and
+ `OwnedNames` keeps only the names, the flag and the cursor, so a client cannot
+ yet see why an account is in use.
+- **No caching.** One owned-by is many `eth_call`s, so it is forked on the
+ server the same way RSLV is, and nothing is memoised.
+- **Enumeration is not atomic.** `balanceOf` and each `tokenOfOwnerByIndex` are
+ separate calls at `latest`, so a transfer between them can duplicate or skip an
+ entry, and successive pages can straddle blocks. Pinning every call to one
+ block number is the fix if it ever matters for a recovery scan.
diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs
index 6d4f9951f..d3f32ec61 100644
--- a/src/Simplex/Messaging/Transport.hs
+++ b/src/Simplex/Messaging/Transport.hs
@@ -178,6 +178,7 @@ smpBlockSize = 16384
-- 20 - public namespaces resolver, RSLV command (6/20/2026)
-- 21 - server public information in handshake (7/5/2026)
-- 22 - RNAME answers name availability as well as the record (7/25/2026)
+-- 23 - ROWN command, the names an address owns (9/22/2026)
data SMPVersion