A Distro story viewer built for Claude Desktop and Claude Cowork: the story opens in Claude's built-in browser pane at essentially zero context cost, premium reads settle over x402, and every purchase comes back as an itemized receipt.

This is the Claude sibling of distro-story-viewer-chatgpt. The two solve the same problem against different runtimes, and the difference is the whole point: ChatGPT needs Distro's HUD HTML converted into an inline visualization fragment, so that build ships a Python helper. Claude has a real browser pane, so this build opens the live page instead and needs no supporting script at all.

Install

Create a folder named distro-story-viewer-claude and save the code block below as SKILL.md, preserving its contents. There is no scripts/ directory and no supporting script — everything the skill needs is inline in the instructions.

Or ask Claude:

"Please fetch the distro-story-viewer-claude skill from Distro Skills and give me a .skill file I can install."

Claude will pull this story, extract the SKILL.md from the fenced block below, and package it as distro-story-viewer-claude.skill. The file appears in your chat with a Save skill button — click it to install. Requires the Distro Reader or Distro Publisher MCP connected so Claude can pull the story.

Claude Code users: drop the folder into ~/.claude/skills/distro-story-viewer-claude/ and restart the session.

No Distro MCP connected? Copy the SKILL.md contents into a file called SKILL.md inside a folder called distro-story-viewer-claude, zip it (folder as zip root), and upload through Settings → Capabilities → Skills → Upload skill.

What you'll need

  • Claude Desktop or Claude Cowork. The skill depends on the built-in browser pane (Claude_Browser__*). It has no meaning in other clients — that is what the ChatGPT build is for.
  • Distro Reader MCP, for distro_story_read and distro_story_render_hud. No Distro account is required; the reader this skill is written for is unauthenticated and pays per article.
  • Base MCP, only for premium stories. Free stories need no wallet.

What it does

  • Opens every story in the pane first, free or premium. The pane keeps a persistent profile across sessions, so a premium story may simply open with no payment at all.
  • Runs x402 when a paywall appears, and presents the approval link as its own message rather than leaving you watching a spinner.
  • Never predicts the settlement. Ownership is per wallet, so the skill says "this may settle as already-owned at no charge, or it may charge $X — I'll tell you which," then reports the truth.
  • Prints a purchase receipt: amount, asset, network, recipient, on-chain transaction, and permanent ownership. Only fields actually present in the settlement payload — nothing reconstructed from the paywall quote. A read that settled as already-owned gets a no-charge block instead, never a payment receipt.
  • Opens the handoff link immediately. It is single-use and expires in about 15 minutes, so the skill opens it rather than handing it over to click later.
  • Tells you how to reopen it for free. New in v3.4: the closing line names the exact words — "Open that article again." — so a reader who comes back an hour later mints a fresh link instead of paying twice.
  • Refuses to link a premium story's canonical URL. The x402 token does not authenticate you on the website, so that link would send a paying reader back to the paywall they just paid through. Premium stories are cited as plain text.
  • Keeps a slow path for artifact saves and for when the pane is offline, with a documented tier order that keeps the page bytes out of the conversation's context.

When to use it

  • Reading, opening, or pulling up a Distro or DeAI News story in Claude Desktop or Cowork.
  • Buying and reading a premium Distro article with a Base wallet.
  • Saving a story to the artifact gallery for later.

When not to use it

  • You want the text, a quote, a summary, or a translation — that comes from the story body, not the pane.
  • You only ran a feed or search listing; those never carry a body.
  • You are publishing or editing stories (Distro Publisher tools).
  • You are in ChatGPT or Codex — install distro-story-viewer-chatgpt instead.

The skill source

This is the canonical SKILL.md:

---
name: distro-story-viewer-claude
description: Use when a user asks to read, open, view, show, or retrieve a Distro article or newslode story in Claude Desktop or Claude Cowork. Fires automatically the moment distro_story_read returns a full article body. Free stories open at their canonical URL in Claude's built-in browser pane; premium stories open via a single-use handoff link after an x402 payment settles, and the purchase is reported as an itemized receipt. Do NOT use when the user asks for the text, markdown, a quote or a summary (answer from the story body), for feed or search listings (no body), for publishing or editing stories (Distro Publisher tools), or for non-Distro web pages.
---

# Distro Story Viewer (v3.4 — browser-pane delivery + purchase receipt)

## What this does and why

Reading a Distro story should cost one page load, not a render pipeline. v3.0
delivers the story by opening it in Claude's built-in browser pane: the real
page on the real site, always current, at essentially zero context cost.

This replaces v2.4's default path, which fetched a self-contained HTML document
from `distro_story_render_hud`, wrote it to disk, and delivered it with
`SendUserFile`. That path still exists — see **The slow path** below — but it is
now used only for artifact saves and when the pane is unreachable.

**Who this is for.** The primary user is an **unauthenticated reader** coming
through Distro Reader — no Distro account, no sign-in, paying per article with
x402. Distro Publisher users are a secondary case. Write every default path for
the unauthenticated reader; account-holder shortcuts are narrow exceptions, not
the happy path.

**Claude only.** This skill depends on the built-in browser pane
(`mcp__remote-devices__Claude_Browser__*`) in Claude Cowork / Claude Desktop.
If those tools are absent, the pane is not available; go straight to the slow
path or hand over markdown.

**A retired design choice.** Earlier versions promised "faithful article style,
no site nav" and treated the chrome-free reading shell as the point. v3.0
deliberately gives that up: the reader sees the real site page, nav and all, in
exchange for an instant open. Do not reintroduce a no-nav shell as the default.

**Still true from v1:** never rebuild a local renderer. A hand-rolled Markdown
engine with a vendored `mermaid.min.js` was the fifth independent copy of the
article shell, and every copy re-created the same styling bugs. The shell lives
server-side in `packages/backend/src/mcp/render/storyShell.js`.

**No article-specific state in this file.** Never record a particular story id,
slug, purchase result, or access token here as though it were a standing fact. A
purchase outcome is true for one wallet at one moment, and a note like "verified
already-owned on story N" will be wrong for the next reader — or the next
session. Guidance in this skill must hold for any story, any wallet, any time.

## When to fire

Fire **automatically** the moment `distro_story_read` returns a body. You have
a body when the result contains a non-empty `body` (or `content`) field. You do
**not** have a body — so don't open anything — when:

- The result is a paywall challenge (`paywall.required` / HTTP 402). That is
  not a failure; it is step 2 of the workflow below.
- You only ran `distro_feed_latest` or `distro_search` (those return listings
  and previews, never the body).

**Text-only carve-out:** if the user asked for the text, the markdown, a quote,
or a summary, skip the pane entirely — the `distro_story_read` body is the
deliverable. Opening is for *reading*, not for every mention of a story.

If several stories are fetched at once, open each one (or ask which, if that's
clearly what the user wants). Opening is cheap and non-destructive.

## Workflow

### 1. Open it in the pane

Call `Claude_Browser__preview_start` with the story's canonical URL, then
`Claude_Browser__get_page_text` to confirm what loaded.

Do this **first, for every story, free or premium.** The pane keeps a persistent
profile across sessions, so it may already be authenticated for a premium story,
in which case it just opens and the payment path below is unnecessary. Never
*assume* the pane is unauthenticated — but never assume it is signed in either.
This is an observation about what you might find, not a recommendation to go get
signed in; x402 is the normal path for this skill's reader, not a fallback.

If the site needs approval, call `Claude_Browser__request_access` with scope
`"site"` for `app.distro.media` / `deainews.com` and wait for the answer.

If the page text shows the article, you are done — go to step 3.

### 2. If a paywall appears, run x402

The pane will show "Unlock this story" and a wallet-connect panel. That means
this story is premium and the pane is not authenticated for it.

**a. Initiate.** Call `initiate_x402_request` with the canonical API resource
(`https://app.distro.media/api/public/news/by-slug/<pub-slug>/<story-slug>`),
`method: "GET"`, and `maxPayment` set to the story's `cost`.

**a′. If the permission mode blocks the payment tool.** In Cowork's Auto mode
the Base MCP payment call is denied before it ever runs, with a classifier
message about permissions. This is a mode setting, not a wallet problem, not an
MCP outage, and not something the user's go-ahead in chat can override.

Make **one** attempt. On a denial, stop and say — in plain, instructive words,
not the tool's vocabulary:

> To pay for this story I need the Base wallet, and that's blocked while this
> session is in Auto mode. Switch it to Manual: click where it says **Auto** at
> the bottom of the chat box, and it'll change to read **Manual**. Then tell me
> to retry and I'll send you the approval link.

Adapt the wording, keep the substance: *what* is blocked, *where* to click,
*what the label will change to*, and *what to say next*. Then wait.

Hard rules for this branch:

- **Never re-fire the tool hoping it passes.** Two identical denials is two
  wasted turns; the classifier decision does not change on repetition.
- **Never say "loosen the permission mode," "the auto-mode classifier,"** or
  anything else that assumes the user knows the internals. Name the button.
- **Never route around the denial** — no curl, no alternate MCP, no proxy.
- **Do not offer signing in to Distro as the alternative here.** See the
  publisher-only fallback in Notes; it does not apply to most readers.

**b. Present the approval link as its own message. This is not optional.**
Post the approval URL, the service domain, and the amount, and say plainly that
you are waiting on the user's approval before anything else happens. A user
staring at a spinner with no link does not know a decision is theirs to make;
this has already cost one session several minutes of silence.

**c. Wait visibly.** Poll `get_request_status`. Never wait more than one
interval without saying something — no silent `sleep` loops. If it stays
`pending` past a couple of checks, say so and ask whether to keep waiting.

**d. Complete.** On approval, call `complete_x402_request`. Read
`_distro.x402.settlement` and report **what it actually says**:

- `"settled"` — funds moved. The user was charged. Report the amount and the
  transaction hash.
- `"skipped_already_owned"` — the wallet already owned it and was **not**
  charged. Report as "already purchased, no new charge," never as a payment.

**Never predict the settlement before you have it.** You cannot know from the
paywall, from the publication, or from the fact that the user is the author
whether this wallet already owns this article — ownership is per wallet, and the
signing wallet may not be the one that bought it before. Telling a user a read
will be free and then charging them is worse than saying nothing. Say "this may
settle as already-owned at no charge, or it may charge $X — I'll tell you which"
and then report the truth. If you got it wrong, say so plainly and first.

**e. Open the handoff link.** The paid body carries
`_distro.x402.webHandoff.url` — a **single-use link that expires in about 15
minutes.** Open it in the pane immediately with `preview_start`. Do not hand it
over and wait for the user to click; by the time they come back it may be dead.
This is the one and only token that belongs in a URL — never put the long-lived
`X-Distro-Access-Token` in a query string.

Save the `accessToken` and its expiry **in this session's working context only**
— never into this skill file. Ownership is permanent: re-reading the same story
with the same wallet does not cost again.

**f. Print a receipt.** After the story is open, post a short receipt block —
this is the user's only durable record of the purchase, and it is what makes an
agent-mediated payment feel accountable rather than opaque. Use this shape:

> Payment completed and the full article was returned.
>
> - **Paid:** 0.05 USDC on Base
> - **Recipient:** `0x…`
> - **Status:** Settled and verified on-chain
> - **Ownership:** Permanent — this wallet can re-read without paying again
> - **Transaction:** [View on BaseScan](https://basescan.org/tx/0x…)
> - **Article:** "<headline>" — <publication>

Rules for the receipt:

- **Only print fields you actually have.** Amount, asset, network, recipient
  (`payTo` from the requirement you signed), and transaction hash come from the
  settlement payload / `PAYMENT-RESPONSE` — never from the paywall quote, never
  reconstructed from memory, never guessed. Drop any line whose value you did
  not receive rather than inventing or approximating it.
- **Match the receipt to the settlement.** On `skipped_already_owned` there was
  no charge and no transaction: replace the Paid / Status / Transaction lines
  with "**No charge** — this wallet already owned the article" and keep the
  Ownership and Article lines. Never print a payment receipt for a read that
  cost nothing.
- **The Article line is plain text for premium stories** — headline and
  publication, no hyperlink. The x402 token does not authenticate the reader on
  the website, so a canonical link sends a paying reader back to the paywall
  they just paid through. Linking the single-use handoff URL is acceptable only
  in the same turn it was minted, and say it expires in ~15 minutes. Free
  stories link normally.
- **BaseScan only for Base.** Build the explorer link from the network the
  settlement names; if the network is unfamiliar, print the bare hash.
- Keep it to the block. No paragraph of explanation around it.

### 3. Say what's available next

After the story is open, close with this two-paragraph shape. Use it verbatim
where it fits:

> The handoff link I used expires in about 15 minutes. If it goes stale I can
> mint a fresh one at no charge — no wallet approval needed. (Just tell me,
> "Open that article again.")
>
> I can also save the page to your artifact gallery, though that route takes a
> few minutes.

Two things about that shape are not decoration:

- **Keep the parenthetical cue.** Naming the exact words the reader can type is
  what makes the free re-open discoverable. Without it, "I can mint a fresh
  one" reads as a capability rather than an instruction, and the reader pays
  for the same article twice.
- **Keep the blank line before "I can also."** The refresh offer and the
  artifact save are separate decisions; running them together buries the
  second one.

Adapt the wording when the situation differs — a free story has no handoff link
to refresh, so that paragraph goes away — but keep the cue whenever a refresh is
on the table.

The two offers behind it:

- **Another handoff link.** The first one is single-use and expires fast.
  Call `distro_story_read` on the same story, passing the `accessToken` you
  saved in step 2e, and read `_distro.x402.webHandoff.url` from the response.
  Each call mints a new single-use link with a fresh 15-minute window. Open it
  in the pane immediately.

  No payment, no wallet approval, no settlement. This is a token-authenticated
  read, not a purchase, so there is no receipt to print and nothing to confirm
  before you answer. A whole-book token works the same way for any chapter of
  that book.

  If a response ever lacks `webHandoff.url`, don't re-run x402 to force one —
  say so plainly and fall back to the artifact-gallery save, which is durable
  and needs no link at all. Minting is rate-limited per token, so treat a
  missing link as an answer, not something to retry in a loop.
- **Save it as an artifact.** Uses the slow path below; warn that it takes a
  few minutes.

**Don't cite the canonical web URL for a premium story.** The x402 access token
does not authenticate the reader on the website, so linking the article in chat
(`Sources:` line included) sends a paying reader to a paywall they already paid
through. Cite premium stories as plain text — headline, publication, author,
date, no hyperlink. Free stories cite normally, with the link.

## The slow path (artifact saves, and when the pane is offline)

Use this **only** when the user asks to save the story to the artifact gallery,
or when the browser pane is unavailable (its tools are absent, or its calls
can't reach the desktop app). It is the only path that produces a `file_uuid`.

**Tell the user the cost up front:** an image-bearing story takes roughly 4–7
minutes of wall time, because the page has to be regenerated token by token to
reach disk. The chat stays usable meanwhile. Say this BEFORE starting — a
silent multi-minute spinner reads as a hang. For a fast skim, offer
`maxHtmlBytes: 12000`, which returns in ~1–2 minutes but degrades images to
remote URLs that will NOT display in the sandboxed panel; only with the user's
ok, and say what was traded away.

**Get the bytes to disk without routing them through your context.** Everything
a tool returns stays in the conversation history for every later turn. Use the
first tier that works:

**a. Render URL (cheapest, ~200 tokens).** Call `distro_story_render_hud` with
the identifier you read the story by (one of `storyId`, `slug`
`"<publication-slug>/<story-slug>"`, or `url`) plus `htmlDelivery: "link"` (and
`accessToken` for premium). The result carries `renderUrl`. Then:

```
curl -sS -o story.html -H "X-Distro-Access-Token: <token-if-premium>" "<renderUrl>"
```

Confirm the file starts with `<!DOCTYPE html>` (a JSON body means a 402 or
error — read it). If curl fails at the network layer (e.g. `CONNECT tunnel
failed, response 403` — the Cowork container egress proxy blocks
non-allowlisted domains as of 2026-08), that is an EXPECTED, known condition:
make exactly ONE attempt, then fall through to (b) immediately. Do NOT diagnose
the failure, read proxy configuration or READMEs, retry via `$HTTPS_PROXY`, or
attempt any other network route — network debugging is never part of this skill.

**b. Subagent fetch (default).** Spawn a subagent with the Agent tool —
subagents inherit this session's MCP connections and share the filesystem, and
their context is discarded when they finish, so the payload is paid once and
never enters this conversation's history. If the Agent tool exists you MUST use
this tier — (c) is only for when it is absent. An interrupted, stopped, or
failed earlier attempt says NOTHING about which tier to use next: the hierarchy
resets fresh for every story. Never infer from a stop button, error, or timeout
that the user has rejected delegation; if genuinely unsure, ask in one sentence.

Give the subagent this errand VERBATIM (the constraints exist because a
2026-08-20 field test burned ~10 minutes when a subagent tried the renderUrl
itself and went network-debugging):

> Call `distro_story_render_hud` with `{<same identifier>,
> selfContained: true<, accessToken: …>}` (load it via ToolSearch first if
> needed). Write the `structuredContent.html` string VERBATIM to
> `<absolute path>/story.html` — if the client reports the result was too
> large and spilled to a temp file, recover the html field from that file
> and write it out. Do not summarize, edit, or re-encode it.
>
> HARD CONSTRAINTS: your ONLY actions are that one tool call, the file
> write, and one `wc -c` on the written file. Compare the byte count to
> the length of the html string you received — if they differ by more
> than a few bytes, rewrite the file ONCE. The result will contain a
> `renderUrl` — IGNORE it: do not curl it or any other URL (network
> egress is blocked here; that is expected, not a problem to solve). Do
> not read any other files, do not inspect proxy or network
> configuration, do not explore. Return ONLY the file path, the byte
> count, and the story title.

**c. Direct inline call (last resort, only if the Agent tool is absent).** Call
`distro_story_render_hud` with `selfContained: true` and write
`structuredContent.html` to `story.html` yourself.

**Degradation disclosure (every tier, no exceptions).** If any render result
carries a `degraded` field (e.g. `["images-not-inlined"]`), tell the user exactly
what was degraded and that remote-URL images will NOT display in the sandboxed
side panel — BEFORE or WITH delivery, never silently.

**Deliver it:**

```
SendUserFile(files=["story.html"], display="render", status="normal",
             caption="<title> — Distro reading page.")
```

`display: "render"` marks the file as viewable. It does NOT reliably open the
side panel on its own: it delivers a **clickable file card**, and the desktop
client auto-opens the panel only for pages under a size ceiling.

Measured by bisection 2026-08-21 (Cowork desktop). Auto-opened: 878 B; 1,257 B
with external Google Fonts links; 12,023 B with an embedded data URI; 14,948 B;
17,001 B; 24,601 B; 23,989 B with a 22,096-char data URI; and the real story
page trimmed to 27,241 B. Did NOT auto-open, twice: the untrimmed 29,906 B
story page. **The ceiling is between 27,241 and 29,906 bytes — probably 28,672
(28KiB).** Ruled out along the way: external references, embedded images,
data-URI length, and panel occupancy.

Therefore: NEVER tell the user the story "is open in the side panel" — you get no
signal back about what the panel did, and a successful send is
indistinguishable from one that sat there unnoticed. Say something like *"Click
the card above to open it in the side panel."*

**Then save it.** `SendUserFile` returns a `file_uuid`; pass it to
`mcp__remote-devices__create_artifact` with a kebab-case id derived from the
story's own slug and a one-line description. If no desktop is connected, that
tool is absent — note the page is delivered and can be saved once the desktop
app is open.

## Notes and edge cases

- **If everything fails** (pane unreachable, render call errors, MCP down), say
  so plainly and give the user the story as markdown in chat from the
  `distro_story_read` body. Do **not** attempt to build the page locally — a
  hand-rolled fallback is exactly the drifted shell v2 removed.
- **Books:** `distro_book_render_hud` works the same way on the slow path
  (`htmlDelivery: "link"` → subagent → direct `selfContained: true`), with a
  whole-book `accessToken` for owned books; its `renderUrl` lives at
  `/api/public/newslodes/<id>/render`. For reading, the pane and the book's
  canonical URL are still the fast path. A book purchase gets the same receipt
  block as step 2f, naming the book instead of the article.
- **Handoff refresh.** `POST /api/public/news/<id>/handoff-access` is still
  unreachable from the Cowork container (blocked egress), and MCP calls still
  can't carry the token header. Neither matters now: as of Distro v0.42.0,
  `distro_story_read` and `distro_story_render_hud` mint a fresh
  `_distro.x402.webHandoff` whenever they're passed a valid `accessToken` for
  that story. A refresh is one tool call and no wallet interaction. The token
  stays article- or book-scoped, so it can't mint a link for anything the
  wallet didn't buy.
- **Publisher-only fallback: signing the pane in to Distro.** A publisher
  reading a premium article **in a newslode they own** can sign the browser pane
  in to Distro once; the pane keeps its profile across sessions, so their own
  premium articles then open at the canonical URL with no x402 at all.

  This is a rare, narrow case. **Do not offer it in any default workflow.**
  Raise it only when the user's own words make it clearly applicable — they say
  they're the publisher of this newslode, they mention having an account, they
  ask whether they can avoid paying for their own work, or they hit repeated
  payment friction on articles they own. For everyone else — the ordinary
  Distro Reader user with no account — mentioning sign-in is noise at best and
  a dead end at worst. x402 is the path.
- **Featured images and diagram label colors are the server's job.** They
  arrive correct in the rendered page. Never fetch an image with the browser or
  pass one in by hand. If Mermaid labels ever look wrong, that's a backend bug
  to report, not something to patch here.
- **Full HTML blocks render as authored** — Distro stories may embed raw HTML
  and the server passes it through. These are the publisher's own trusted
  stories; that passthrough is intended.

Feedback

This is v3.4. Edge cases or suggestions → reply or ping Bradley Keoun.