From 35f3b1c395ff009cf11433a54943e642c2bb47ce Mon Sep 17 00:00:00 2001
From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com>
Date: Mon, 27 Jul 2026 19:58:43 +0530
Subject: [PATCH] feat(tinyplace): resolve identity marketplace seller-side as
web-only (#4920) (#5193)
---
.../pages/IdentitiesSection.test.tsx | 31 +++
.../agentworld/pages/IdentitiesSection.tsx | 45 ++++
...24-identity-marketplace-seller-web-only.md | 212 ++++++++++++++++++
...tity-marketplace-seller-web-only-design.md | 104 +++++++++
src/openhuman/tinyplace/mod.rs | 10 +
5 files changed, 402 insertions(+)
create mode 100644 docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md
create mode 100644 docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md
diff --git a/app/src/agentworld/pages/IdentitiesSection.test.tsx b/app/src/agentworld/pages/IdentitiesSection.test.tsx
index 9f8d2b497..59f50e6d0 100644
--- a/app/src/agentworld/pages/IdentitiesSection.test.tsx
+++ b/app/src/agentworld/pages/IdentitiesSection.test.tsx
@@ -17,6 +17,7 @@ import {
type DirectoryIdentityListingsResponse,
PaymentRequiredError,
} from '../../lib/agentworld/invokeApiClient';
+import { openUrl } from '../../utils/openUrl';
import { apiClient } from '../AgentWorldShell';
import IdentitiesSection, { IDENTITY_PRICING_TIERS } from './IdentitiesSection';
@@ -39,6 +40,11 @@ vi.mock('../AgentWorldShell', () => ({
},
}));
+// External-link opener — assert the seller CTA hands off to the OS browser
+// without actually invoking Tauri.
+// openUrl returns a Promise (the CTA chains .then/.catch for diagnostics).
+vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn(() => Promise.resolve()) }));
+
// Default happy-path resolutions so async hooks settle without unhandled
// rejections. Individual tests override per-case.
beforeEach(() => {
@@ -1282,3 +1288,28 @@ describe('IDENTITY_PRICING_TIERS', () => {
expect(screen.queryByText('$10/yr')).not.toBeInTheDocument();
});
});
+
+// Unit coverage only, by design: the affordance is a static info note whose CTA
+// hands off to the OS browser via the mocked `openUrl`. There is one real
+// cross-process effect (that the OS browser actually opens) but it isn't worth a
+// desktop WDIO E2E — these unit tests cover the note + the openUrl call. E2E is
+// intentionally skipped for this change (#4920).
+describe('Trading tab — seller web-only note', () => {
+ test('renders the seller pointer note on the Trading tab', async () => {
+ render();
+ await gotoTab('Trading');
+ const note = await screen.findByTestId('sell-on-web');
+ expect(note).toHaveTextContent(/listing a handle for sale and accepting offers/i);
+ expect(screen.getByTestId('sell-on-web-cta')).toHaveAttribute(
+ 'data-analytics-id',
+ 'identities.sellOnWeb'
+ );
+ });
+
+ test('CTA opens the tiny.place identities page via openUrl', async () => {
+ render();
+ await gotoTab('Trading');
+ await userEvent.click(await screen.findByTestId('sell-on-web-cta'));
+ expect(vi.mocked(openUrl)).toHaveBeenCalledWith('https://tiny.place/identities');
+ });
+});
diff --git a/app/src/agentworld/pages/IdentitiesSection.tsx b/app/src/agentworld/pages/IdentitiesSection.tsx
index 47fff1ddb..d173f8a96 100644
--- a/app/src/agentworld/pages/IdentitiesSection.tsx
+++ b/app/src/agentworld/pages/IdentitiesSection.tsx
@@ -12,7 +12,14 @@
* Offer commitments (signed authorizations — funds move only on acceptance).
* Money only moves after the user confirms. The read-only data views (Registry
* listing, floor prices, recent sales) are fully functional.
+ *
+ * Seller-side *writes* (list a handle for sale, accept / reject an offer/bid)
+ * are intentionally NOT in-app — they are web-only on tiny.place (the backend
+ * exposes no seller write routes). Seller-side *reads* do exist (offers on your
+ * handles, via marketplace_list_offers). The Trading tab links sellers to the
+ * web app for the write actions it cannot perform (#4920).
*/
+import debugFactory from 'debug';
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
import ChipTabs from '../../components/layout/ChipTabs';
@@ -31,12 +38,21 @@ import {
type RegistrationChallenge,
type RegistryWalletBalance,
} from '../../lib/agentworld/invokeApiClient';
+import { openUrl } from '../../utils/openUrl';
import { apiClient } from '../AgentWorldShell';
import { decimalsForAsset, formatAssetAmount } from '../assets';
import CommitFlow from '../components/CommitFlow';
import X402ConfirmDialog from '../components/X402ConfirmDialog';
import { explorerTxUrl as buyExplorerTxUrl, useX402Buy } from '../hooks/useX402Buy';
+const debug = debugFactory('agentworld:identities');
+
+// Seller-side identity *writes* (list a handle for sale, accept/reject an offer)
+// are web-only — the tiny.place backend exposes no seller write routes (see
+// #4920). We point sellers at the web app for those. Hardcoded prod URL,
+// matching the `FUND_PAGE_URL` precedent in X402ConfirmDialog.tsx.
+const SELL_ON_WEB_URL = 'https://tiny.place/identities';
+
// ── Types ─────────────────────────────────────────────────────────────────────
type Tab = 'register' | 'registry' | 'trading';
@@ -945,6 +961,35 @@ function TradingTab() {
)}
+ {/* Seller-side *writes* (list-for-sale / accept-reject offer) are web-only
+ (#4920): the backend exposes no seller write routes. Placed directly
+ under the listings — where someone who came to sell looks — rather than
+ below Recent Sales. Viewing offers on your handles is a separate read
+ that does exist (marketplace_list_offers). */}
+
+
+ Listing a handle for sale and accepting offers happens on tiny.place.
+
+
+
+
{/* Recent sales */}
diff --git a/docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md b/docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md
new file mode 100644
index 000000000..87ff802cc
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md
@@ -0,0 +1,212 @@
+# Identity Marketplace Seller-Web-Only Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the seller-side dead-end in the desktop Identities → Trading tab with a note pointing users to the web flow, and record that seller-side is web-only by design.
+
+**Architecture:** Additive UI only. A persistent info note at the bottom of `TradingTab` opens `https://tiny.place/identities` via the existing `openUrl()` helper (mirrors the `X402ConfirmDialog` `/fund` precedent). No SDK, core, or `invokeApiClient` changes — the tiny.place backend has no seller routes. Scoping recorded in two doc comments.
+
+**Tech Stack:** React 19 + TypeScript, Vitest + Testing Library, `@tauri-apps/plugin-opener` (via `app/src/utils/openUrl.ts`), Rust doc comment.
+
+## Global Constraints
+
+- **No i18n:** `IdentitiesSection.tsx` is entirely hardcoded English (e.g. `"Commitment submitted."` line 854, `"Purchased {name}"` line 887). New strings are hardcoded English to match. **No locale-file edits** — do NOT add `useT()` here.
+- **Web URL is a hardcoded prod constant:** `https://tiny.place/identities`. Mirrors `FUND_PAGE_URL = 'https://tiny.place/fund'` in `X402ConfirmDialog.tsx`. The tiny.place *web* frontend has no per-env base in `config.ts`.
+- **External links go through `openUrl`** from `app/src/utils/openUrl.ts` — never a raw `window.open` or `` for the CTA (the `` explorer links elsewhere in the file are read-only tx links, a different case).
+- **Out of scope (do not touch):** `vendor/tinyplace/sdk/**`, `src/openhuman/tinyplace/manifest.rs`, `app/src/lib/agentworld/invokeApiClient.ts`, buy/bid/offer flows.
+- **Run commands from repo root** unless noted. The worktree root is `.claude/worktrees/fix-4920`.
+
+---
+
+### Task 1: Seller "sell on tiny.place" note in the Trading tab
+
+**Files:**
+- Modify: `app/src/agentworld/pages/IdentitiesSection.tsx` (imports ~16-37; header docstring ~1-16; `TradingTab` return, insert before line 996 `
` that closes the outer `space-y-4`)
+- Test: `app/src/agentworld/pages/IdentitiesSection.test.tsx` (add a `describe` block; add an `openUrl` mock near the top mocks)
+
+**Interfaces:**
+- Consumes: `openUrl(url: string): Promise` from `../../utils/openUrl`; existing `Button` component from `../../components/ui/Button`.
+- Produces: a rendered note with testid `sell-on-web` containing a CTA button testid `sell-on-web-cta` that calls `openUrl('https://tiny.place/identities')`.
+
+- [ ] **Step 1: Add the `openUrl` mock to the existing test file**
+
+In `app/src/agentworld/pages/IdentitiesSection.test.tsx`, directly after the existing `vi.mock('../AgentWorldShell', …)` block (ends ~line 41), add:
+
+```ts
+// External-link opener — assert the seller CTA hands off to the OS browser
+// without actually invoking Tauri.
+vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn() }));
+```
+
+And add its import next to the other imports at the top of the file (after the `IdentitiesSection` import line):
+
+```ts
+import { openUrl } from '../../utils/openUrl';
+```
+
+- [ ] **Step 2: Write the failing test**
+
+Append this `describe` block to `app/src/agentworld/pages/IdentitiesSection.test.tsx` (after the last Trading-tab describe block; reuses the file's existing `gotoTab` helper, `render`, `screen`, `userEvent`):
+
+```ts
+describe('Trading tab — seller web-only note', () => {
+ test('renders the seller pointer note on the Trading tab', async () => {
+ render();
+ await gotoTab('Trading');
+ const note = await screen.findByTestId('sell-on-web');
+ expect(note).toHaveTextContent(/selling a handle or responding to offers/i);
+ });
+
+ test('CTA opens the tiny.place identities page via openUrl', async () => {
+ render();
+ await gotoTab('Trading');
+ await userEvent.click(await screen.findByTestId('sell-on-web-cta'));
+ expect(vi.mocked(openUrl)).toHaveBeenCalledWith('https://tiny.place/identities');
+ });
+});
+```
+
+- [ ] **Step 3: Run the test to verify it fails**
+
+Run: `pnpm --filter openhuman-app test -- IdentitiesSection --run -t "seller web-only"`
+Expected: FAIL — `Unable to find an element by: [data-testid="sell-on-web"]`.
+
+- [ ] **Step 4: Add the `openUrl` import + web-URL constant to the component**
+
+In `app/src/agentworld/pages/IdentitiesSection.tsx`, add the import. Import order groups deeper relative paths first, so place it directly **above** `import { apiClient } from '../AgentWorldShell';` (line 33):
+
+```ts
+import { openUrl } from '../../utils/openUrl';
+```
+
+Then, immediately after the import block (before the `// ── Types ──` comment at line 39), add the constant:
+
+```ts
+// Seller-side identity actions (list a handle for sale, accept/reject an offer)
+// are web-only — the tiny.place backend exposes no seller routes and the
+// vendored SDK is a buyer-side compatibility wrapper (see #4920). We point
+// sellers at the web app instead. Hardcoded prod URL, matching the
+// `FUND_PAGE_URL` precedent in X402ConfirmDialog.tsx.
+const SELL_ON_WEB_URL = 'https://tiny.place/identities';
+```
+
+- [ ] **Step 5: Insert the note JSX in `TradingTab`**
+
+In the same file, insert the following block immediately **before** line 996 (the `` that closes the outer `
`), i.e. after the Recent Sales `
` at line 995:
+
+```tsx
+ {/* Seller-side is web-only (#4920): no in-app list-for-sale / accept-offer
+ path exists, so point sellers at the tiny.place web app. */}
+
+
+ Selling a handle or responding to offers happens on tiny.place.
+
+
+
+```
+
+- [ ] **Step 6: Update the header docstring to record the web-only scope**
+
+In the header docstring, extend the `Write flows are live x402:` list (ends with the "Money only moves…" sentence, ~line 12-14). Add a paragraph immediately before the closing `*/` (line 15):
+
+```text
+ *
+ * Seller-side actions (list a handle for sale, accept / reject an offer/bid)
+ * are intentionally NOT in-app — they are web-only on tiny.place. The backend
+ * exposes no seller routes and the vendored SDK is a buyer-side compatibility
+ * wrapper, so the Trading tab links sellers to the web app instead (#4920).
+```
+
+- [ ] **Step 7: Run the tests to verify they pass**
+
+Run: `pnpm --filter openhuman-app test -- IdentitiesSection --run`
+Expected: PASS — the new `seller web-only` tests pass and the pre-existing IdentitiesSection tests still pass.
+
+**E2E scope (approved exception):** no desktop WDIO E2E is added for this change. The affordance is a static info note whose CTA hands off to the OS browser via `openUrl` — there is no cross-process behavior an E2E could assert beyond the unit tests above. This is a documented exception to the standard unit-and-E2E expectation; the exception is also recorded in the design spec's Testing section and in a comment on the test's `describe` block.
+
+- [ ] **Step 8: Typecheck, lint, and format**
+
+Run: `pnpm typecheck && pnpm --filter openhuman-app lint -- --fix app/src/agentworld/pages/IdentitiesSection.tsx app/src/agentworld/pages/IdentitiesSection.test.tsx && pnpm --filter openhuman-app exec prettier --write app/src/agentworld/pages/IdentitiesSection.tsx app/src/agentworld/pages/IdentitiesSection.test.tsx`
+Expected: no type errors, no lint errors, files formatted (import order may be auto-fixed).
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add app/src/agentworld/pages/IdentitiesSection.tsx app/src/agentworld/pages/IdentitiesSection.test.tsx
+git commit -m "feat(tinyplace): point sellers to tiny.place web from Trading tab (#4920)"
+```
+
+---
+
+### Task 2: Record web-only scope in the tinyplace core domain doc
+
+**Files:**
+- Modify: `src/openhuman/tinyplace/mod.rs` (module doc comment, after the `## Seed derivation` block ending line 26)
+
+Docs-only; no test (Rust doc comment). Folded into a single commit.
+
+- [ ] **Step 1: Add a scoping note to the module doc**
+
+In `src/openhuman/tinyplace/mod.rs`, insert this doc-comment section immediately after line 26 (`//! The seed is never logged…`) and before the blank line preceding `pub(crate) mod agent;`:
+
+```rust
+//!
+//! ## Marketplace scope (buyer-side only)
+//!
+//! The identity marketplace handlers here are **buyer-side only**: buy a
+//! listing, bid, and make an offer. Seller-side actions — listing a handle for
+//! sale and accepting / rejecting offers — are **web-only on tiny.place** (the
+//! backend exposes no seller routes). The desktop Trading tab links sellers to
+//! the web app instead. See #4920.
+```
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml`
+Expected: compiles (doc comment only — no code change). Warnings unrelated to this change are fine.
+
+- [ ] **Step 3: Format**
+
+Run: `cargo fmt --manifest-path Cargo.toml`
+Expected: no diff on the doc-comment lines (doc comments are not reflowed).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/openhuman/tinyplace/mod.rs
+git commit -m "docs(tinyplace): note identity marketplace is buyer-side only, selling is web-only (#4920)"
+```
+
+---
+
+### Task 3: GitHub bookkeeping (outward-facing — CONFIRM before running)
+
+**Not code.** Do NOT run these until the user explicitly confirms editing the epic and commenting on the issue (both are outward-facing). Until then, capture them in the PR body.
+
+- [ ] **Step 1: Comment the resolution on #4920**
+
+```bash
+gh issue comment 4920 --repo tinyhumansai/openhuman --body "Resolved as **web-only by design**: the tiny.place backend exposes no seller routes and the vendored SDK is a buyer-side compatibility wrapper, so listing a handle / accepting-rejecting offers stays on the tiny.place web app. The desktop Trading tab now points sellers there instead of dead-ending. Scope recorded in \`IdentitiesSection.tsx\` and \`src/openhuman/tinyplace/mod.rs\`. See PR ."
+```
+
+- [ ] **Step 2: Mark #4776 §9 seller items N/A**
+
+Edit the #4776 body: change the "Sell / list a handle for sale" and "Accept / reject offer" table rows' Status to `N/A (web-only — #4920)`, and change `- [ ] Sell / list flow works` to `- [x] Sell / list flow works — N/A, web-only (#4920)`. (Manual body edit via `gh issue edit 4776 --repo tinyhumansai/openhuman --body-file -` after fetching and patching the current body, to avoid clobbering other sections.)
+
+---
+
+## Self-Review
+
+- **Spec coverage:** UI affordance → Task 1 (steps 4-5). Hardcoded-English constraint → Global Constraints + Task 1 (no `useT`). Docstring scoping → Task 1 step 6 + Task 2. Test → Task 1 steps 1-3, 7. GitHub bookkeeping → Task 3. Out-of-scope items → Global Constraints. All spec sections covered.
+- **Placeholder scan:** `` / `` in Task 3 are intentional fill-at-runtime values for outward-facing text, not code placeholders. No code step contains TBD/TODO.
+- **Type consistency:** testids `sell-on-web` / `sell-on-web-cta` and constant `SELL_ON_WEB_URL` used identically in test (Task 1 steps 2) and component (Task 1 steps 4-5). `openUrl` signature matches `app/src/utils/openUrl.ts`.
diff --git a/docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md b/docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md
new file mode 100644
index 000000000..ad566ab32
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md
@@ -0,0 +1,104 @@
+# Design: Identity marketplace seller-side is web-only (#4920)
+
+**Date:** 2026-07-24
+**Issue:** [#4920](https://github.com/tinyhumansai/openhuman/issues/4920) — *[Tiny Place] Identity marketplace is buyer-only: no list-for-sale or accept/reject-offer path*
+**Epic:** #4776 §9 (Trading / Identities marketplace)
+
+## Problem
+
+The desktop app's identity marketplace (Agent World → Identities → Trading tab) supports
+only **buyer-side** actions: browse listings, buy, bid, and make an offer. There is no path
+to **sell** (list one's own `@handle`) and no path to **accept / reject** an incoming offer
+or bid. A user who wants to sell opens the Trading tab and finds a dead end.
+
+## Decision
+
+**Seller-side actions are web-only by design.** The tiny.place backend does not expose
+seller routes (create-listing, accept/reject-offer), and the vendored Rust SDK
+(`vendor/tinyplace/sdk/rust/src/api/marketplace.rs`) is explicitly a *"compatibility wrapper
+for marketplace endpoints used by OpenHuman"* — buyer-side only. Listing a handle and
+responding to offers happens on the tiny.place web app.
+
+This resolves #4920 not by building the full seller stack (SDK → core handler →
+`invokeApiClient` → UI), but by:
+
+1. Removing the in-app dead-end with a clear pointer to the web flow.
+2. Recording the web-only scoping decision in-repo.
+3. Marking the corresponding #4776 §9 items N/A.
+
+## Scope
+
+### In scope
+
+1. **UI affordance** — `app/src/agentworld/pages/IdentitiesSection.tsx`, Trading tab.
+ Add an informational note beneath the listings / recent-sales area:
+
+ > ℹ️ Selling a handle or responding to offers happens on tiny.place. **[Open tiny.place →]**
+
+ - The CTA opens `https://tiny.place/identities` via `openUrl()`
+ (`app/src/utils/openUrl.ts` → `tauri-plugin-opener`), mirroring the existing
+ `https://tiny.place/fund` precedent in `X402ConfirmDialog.tsx`.
+ - The web URL is a hardcoded production constant, matching the `/fund` precedent
+ (the tiny.place *web* frontend has no per-env base in `config.ts`; only the API
+ host is env-derived).
+ - Strings are **hardcoded English**, matching this file's existing convention. Unlike
+ sibling agentworld pages, `IdentitiesSection.tsx` is entirely un-i18n'd (its buy/commit
+ banners use literal strings, e.g. `"Commitment submitted."`, `"Purchased {name}"`), and
+ this passes CI (`i18n:check` / `i18n:english:check` validate locale-file parity, not
+ hardcoded JSX). Introducing a single `useT()` string here would be inconsistent; a
+ full-file i18n conversion is out of scope. No locale-file changes.
+
+2. **Scoping documentation**
+ - Extend the `IdentitiesSection.tsx` header docstring to state that seller-side
+ (list-for-sale, accept/reject offer) is **web-only by design**, with the reason.
+ - Add a one-line pointer in the tinyplace domain module doc
+ (`src/openhuman/tinyplace/mod.rs`).
+
+3. **Test** — co-located Vitest test asserting the Trading tab renders the seller note and
+ the CTA invokes `openUrl` with `https://tiny.place/identities` (mock `openUrl`). Covers
+ the new diff lines for the ≥80% changed-line coverage gate.
+
+4. **GitHub bookkeeping** (not code; done via `gh`, noted in the PR body)
+ - #4776 §9: mark "Sell / list a handle for sale", "Accept / reject offer", and the
+ "Sell / list flow works" checkbox as N/A, referencing #4920's resolution.
+ - Comment the web-only resolution on #4920.
+
+### Out of scope (YAGNI)
+
+- Vendored SDK seller methods (`create_listing` / `accept_offer` / `reject_offer`).
+- Core `handle_tinyplace_marketplace_*` seller handlers.
+- `invokeApiClient.ts` seller methods.
+- Full i18n conversion of `IdentitiesSection.tsx` (the new strings are hardcoded English,
+ matching the file).
+
+## Components & data flow
+
+```text
+Trading tab (IdentitiesSection.tsx)
+ └─ SellOnWebNote ──click──▶ openUrl('https://tiny.place/identities')
+ └─ tauri-plugin-opener → OS default browser
+```
+
+No core RPC, no network from the app itself — the CTA hands off to the OS browser.
+
+## Error handling
+
+`openUrl()` already handles the CEF IPC-bridge gap by falling back to `window.open` for
+http(s) URLs and logs a low-PII telemetry breadcrumb. No new error surface is introduced.
+
+## Testing
+
+- **Unit (Vitest):** render `TradingTab`, assert the seller note text is present, that the
+ CTA carries the `identities.sellOnWeb` analytics id, and that clicking it calls the mocked
+ `openUrl` with the expected URL.
+- No new Rust behavior → no Rust test changes.
+- **E2E: intentionally skipped.** The affordance is a static info note whose CTA hands off to
+ the OS browser via `openUrl` — there is no cross-process behavior a desktop WDIO E2E could
+ assert beyond the unit tests. Documented as an approved exception to the standard
+ unit-and-E2E expectation for this change.
+
+## Rollback / risk
+
+Low risk: additive UI note + docstring. No behavior change to existing buy/bid/offer flows,
+and no i18n/locale files touched (strings are hardcoded English, matching the file). Revert
+is a clean removal of the note block plus its docstring note and the two unit tests.
diff --git a/src/openhuman/tinyplace/mod.rs b/src/openhuman/tinyplace/mod.rs
index c99f90f04..f42c1b3b6 100644
--- a/src/openhuman/tinyplace/mod.rs
+++ b/src/openhuman/tinyplace/mod.rs
@@ -24,6 +24,16 @@
//! access. The seed is derived via the same SLIP-0010 path used for all Solana
//! signing (`m/44'/501'/0'/0'`); the wallet key becomes the tiny.place identity.
//! The seed is never logged, persisted, or returned across any IPC boundary.
+//!
+//! ## Marketplace scope (seller-side writes are web-only)
+//!
+//! The identity marketplace **write** handlers here are buyer-side only: buy a
+//! listing, bid, and make an offer. Seller-side *writes* — listing a handle for
+//! sale and accepting / rejecting offers — are **web-only on tiny.place** (the
+//! backend exposes no seller write routes). Seller-side *reads* do exist:
+//! `marketplace_list_offers` takes a `name` filter for offers targeting a handle
+//! you own. The desktop Trading tab links sellers to the web app for the actions
+//! it cannot perform. See #4920.
pub(crate) mod agent;
mod agent_tools;