Skip to content

Added agent payments for premium markdown URLs#28291

Open
JohnONolan wants to merge 12 commits into
mainfrom
codex/x402-mpp-markdown
Open

Added agent payments for premium markdown URLs#28291
JohnONolan wants to merge 12 commits into
mainfrom
codex/x402-mpp-markdown

Conversation

@JohnONolan
Copy link
Copy Markdown
Member

@JohnONolan JohnONolan commented Jun 1, 2026

Summary

Adds x402 and MPP support for paid-members-only markdown URLs so AI agents can pay per post when machine payments are enabled.

  • gates agent payments behind the machinePayments labs flag and the existing llms.txt setting
  • lists paid post markdown URLs in /llms.txt only when agent payments are enabled
  • serves paid .md URLs through machine-payment challenges using the existing Stripe Connect patterns
  • adds markdown alternate links for eligible posts
  • moves the Admin control into Tiers and disables it when llms.txt is off

Paid-member markdown URLs were previously unavailable to AI agents, so this adds x402/MPP payment challenges backed by Ghost's existing Stripe key-selection flow and exposes the per-post price in the Tiers settings area. The public app dev scripts are adjusted so fresh worktrees can start preview servers after building their UMD bundles.
ref #27400

MPP requires a server-side secret to bind issued challenges to credentials. Using Ghost's existing theme session secret keeps the paid markdown flow working without introducing a separate machine-payments configuration requirement.
ref #27400

Code review found that paid markdown success responses could remain publicly cacheable, MPP reused an unrelated theme session secret, challenge generation failed hard when one rail was unavailable, and repeated unauthenticated requests could create unnecessary Stripe deposit intents. This keeps paid content non-cacheable, separates MPP challenge signing, reuses pending deposit addresses, validates machine payment settings server-side, and avoids charging free custom-tier posts.
ref #27400

Agents need a discoverable path to paid markdown resources before they can receive a machine-payment challenge. Listing paid-members-only markdown URLs when machine payments are enabled exposes purchasable resources without leaking premium content.
ref #27400

The admin production build imports ActivityPub as an embedded app and failed before packaging editor assets because the Zod 4 schema type did not satisfy the resolver overload. Casting the schema at the resolver boundary keeps runtime validation unchanged while allowing the current dependency set to typecheck and produce the admin bundle.
This reverts commit 18c72d321e389298941cb2613d0e880184497207.
ref #27400

Machine-readable markdown URLs for paid posts need a payment challenge path so agents can discover premium content through llms.txt and purchase per-post access through the same Stripe Connect patterns used elsewhere in Ghost.

This keeps the feature behind the llms.txt and machinePayments labs flags, exposes paid post markdown alternates only when agent payments are enabled, and verifies the x402/MPP challenge path with focused unit, frontend, and Admin acceptance coverage.
@github-actions github-actions Bot added the migration [pull request] Includes migration for review label Jun 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

It looks like this PR contains a migration 👀
Here's the checklist for reviewing migrations:

General requirements

  • ⚠️ Tested performance on staging database servers, as performance on local machines is not comparable to a production environment
  • Satisfies idempotency requirement (both up() and down())
  • Does not reference models
  • Filename is in the correct format (and correctly ordered)
  • Targets the next minor version
  • All code paths have appropriate log messages
  • Uses the correct utils
  • Contains a minimal changeset
  • Does not mix DDL/DML operations
  • Tested in MySQL and SQLite

Schema changes

  • Both schema change and related migration have been implemented
  • For index changes: has been performance tested for large tables
  • For new tables/columns: fields use the appropriate predefined field lengths
  • For new tables/columns: field names follow the appropriate conventions
  • Does not drop a non-alpha table outside of a major version

Data changes

  • Mass updates/inserts are batched appropriately
  • Does not loop over large tables/datasets
  • Defends against missing or invalid data
  • For settings updates: follows the appropriate guidelines

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd6a152f-7123-4715-837d-bce8a54271b0

📥 Commits

Reviewing files that changed from the base of the PR and between 76c3cb9 and f59a97f.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • ghost/core/test/e2e-api/admin/settings.test.js

Walkthrough

Adds machine-payments settings, schema and migration, labs flag, admin UI controls in tiers and beta features, MachinePaymentsService with X402 and MPP adapters and a Stripe deposit-address provider, LLMS markdown and service refactors to include paid entries and backlinks, entry controller routing for paid markdown via machine payments, dependency updates, and extensive unit/e2e/acceptance tests.

Possibly related PRs

  • TryGhost/Ghost#28260: Both PRs modify ghost/core/core/frontend/services/routing/controllers/entry.js to change .md/LLMS markdown request routing and 403/redirect behavior based on feature enablement and visibility.

Suggested labels

preview

Suggested reviewers

  • 9larsons
  • weylandswart
  • mike182uk
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: adding agent payments for premium markdown URLs. It is specific, concise, and clearly conveys the primary change.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, covering the key aspects: gates behind labs flag, lists paid posts in /llms.txt, serves through payment challenges, adds alternate links, and moves admin control to Tiers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/x402-mpp-markdown

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
ghost/core/package.json (1)

186-186: ⚡ Quick win

Pin hono to an exact version for lockstep reproducibility.

Line 186 uses a caret range while the new adjacent machine-payment deps are pinned exactly; this can introduce avoidable drift across installs.

Proposed change
-    "hono": "^4.12.18",
+    "hono": "4.12.18",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/package.json` at line 186, The dependency "hono" is using a caret
range which allows version drift; update the package.json dependency entry for
"hono" from the caret range to an exact version string (change "hono":
"^4.12.18" to "hono": "4.12.18") so installs are reproducible, then regenerate
the lockfile (run your package manager install) to persist the exact version in
the lockfile.
apps/admin-x-settings/test/acceptance/general/seometa.test.ts (1)

67-71: ⚡ Quick win

Replace fixed sleep with condition-based waiting.

Using page.waitForTimeout(100) can make this test flaky under variable CI timing; rely on Playwright’s auto-retrying focus assertions instead.

♻️ Suggested change
         await toggle.uncheck();
-        await page.waitForTimeout(100);
-
-        await expect(section.getByLabel('Meta title')).not.toBeFocused();
-        await expect(toggle).toBeFocused();
+        await expect(toggle).toBeFocused();
+        await expect(section.getByLabel('Meta title')).not.toBeFocused();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin-x-settings/test/acceptance/general/seometa.test.ts` around lines
67 - 71, Remove the fixed sleep after await toggle.uncheck(); and rely on
Playwright’s auto-retrying assertions: after unchecking, assert that the 'Meta
title' field is not focused and that the toggle element is focused using await
expect(section.getByLabel('Meta title')).not.toBeFocused(); and await
expect(toggle).toBeFocused(); (use the existing toggle and
section.getByLabel('Meta title') references) instead of
page.waitForTimeout(100).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/frontend/helpers/ghost_head.js`:
- Around line 204-214: The predicate shouldOutputMarkdownAlternate can return
true even when the llmsTxt lab is off; update the function
(shouldOutputMarkdownAlternate) to require labs.isSet('llmsTxt') to be true
before returning true — e.g., add a check that returns false if
labs.isSet('llmsTxt') is falsy (either in the initial guard or combined with the
final return) so the .md alternate is only advertised when the llmsTxt lab flag
is enabled.

In `@ghost/core/core/frontend/services/llms/service.js`:
- Around line 57-69: getLlmsTxt currently only indexes posts so public pages are
omitted; call fetchPageIndexEntries() as well (or the existing page index
function) and include its results in the sections array (e.g.
buildIndexSection(pages) or merge posts+pages before buildIndexSection). Update
getLlmsTxt to await fetchPageIndexEntries(), add a pages index section (or
combine into posts) alongside buildIndexSection(posts), and preserve the
existing filtering/joining/trim/ending newline behavior.

In
`@ghost/core/core/frontend/services/machine-payments/stripe-deposit-address-provider.js`:
- Around line 28-35: The current flow uses this.#getDepositAddressCacheKey and
cache.get to only reuse completed addresses, so concurrent calls still both miss
and call this.#createAddress (which internally calls
stripe.paymentIntents.create); change the logic to deduplicate in-flight
creations by storing a pending promise/placeholder in the cache under the same
cacheKey before invoking this.#createAddress (or store the promise returned by
this.#createAddress immediately), ensure the cached entry is replaced/updated
with the final resolved address or removed/expired on rejection, and set an
appropriate TTL to avoid stale pending entries so concurrent callers receive the
same pending result instead of creating duplicate PaymentIntents.

---

Nitpick comments:
In `@apps/admin-x-settings/test/acceptance/general/seometa.test.ts`:
- Around line 67-71: Remove the fixed sleep after await toggle.uncheck(); and
rely on Playwright’s auto-retrying assertions: after unchecking, assert that the
'Meta title' field is not focused and that the toggle element is focused using
await expect(section.getByLabel('Meta title')).not.toBeFocused(); and await
expect(toggle).toBeFocused(); (use the existing toggle and
section.getByLabel('Meta title') references) instead of
page.waitForTimeout(100).

In `@ghost/core/package.json`:
- Line 186: The dependency "hono" is using a caret range which allows version
drift; update the package.json dependency entry for "hono" from the caret range
to an exact version string (change "hono": "^4.12.18" to "hono": "4.12.18") so
installs are reproducible, then regenerate the lockfile (run your package
manager install) to persist the exact version in the lockfile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b2f07cfa-8a42-4415-a9ea-b33760ff000f

📥 Commits

Reviewing files that changed from the base of the PR and between 509e457 and 1a13f39.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • apps/admin-x-framework/src/api/settings.ts
  • apps/admin-x-framework/src/test/msw-utils.ts
  • apps/admin-x-framework/src/test/responses/settings.json
  • apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx
  • apps/admin-x-settings/src/components/settings/general/seo-meta.tsx
  • apps/admin-x-settings/src/components/settings/membership/membership-settings.tsx
  • apps/admin-x-settings/src/components/settings/membership/tiers.tsx
  • apps/admin-x-settings/test/acceptance/general/seometa.test.ts
  • apps/admin-x-settings/test/acceptance/membership/tiers.test.ts
  • ghost/core/core/frontend/helpers/ghost_head.js
  • ghost/core/core/frontend/services/llms/markdown.js
  • ghost/core/core/frontend/services/llms/service.js
  • ghost/core/core/frontend/services/machine-payments/adapters/mpp-adapter.js
  • ghost/core/core/frontend/services/machine-payments/adapters/x402-adapter.js
  • ghost/core/core/frontend/services/machine-payments/service.js
  • ghost/core/core/frontend/services/machine-payments/stripe-deposit-address-provider.js
  • ghost/core/core/frontend/services/routing/controllers/entry.js
  • ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js
  • ghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js
  • ghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js
  • ghost/core/core/server/data/migrations/versions/6.31/2026-05-14-18-06-40-add-machine-payments-settings.js
  • ghost/core/core/server/data/schema/default-settings/default-settings.json
  • ghost/core/core/server/models/settings.js
  • ghost/core/core/shared/config/defaults.json
  • ghost/core/core/shared/labs.js
  • ghost/core/package.json
  • ghost/core/test/e2e-frontend/llms.test.js
  • ghost/core/test/unit/frontend/helpers/ghost-head.test.js
  • ghost/core/test/unit/frontend/services/llms/service.test.js
  • ghost/core/test/unit/frontend/services/machine-payments/adapters.test.js
  • ghost/core/test/unit/frontend/services/machine-payments/service.test.js
  • ghost/core/test/unit/frontend/services/machine-payments/stripe-deposit-address-provider.test.js
  • ghost/core/test/unit/frontend/services/routing/controllers/entry.test.js
  • ghost/core/test/unit/server/data/schema/integrity.test.js
  • ghost/core/test/unit/server/models/settings.test.js
  • ghost/core/test/utils/fixtures/default-settings.json
💤 Files with no reviewable changes (1)
  • apps/admin-x-settings/src/components/settings/general/seo-meta.tsx

Comment thread ghost/core/core/frontend/helpers/ghost_head.js
Comment thread ghost/core/core/frontend/services/llms/service.js
ref #28291

Markdown alternate links should not be advertised when the llms.txt lab is disabled, and repeated agent requests should not create duplicate Stripe deposit PaymentIntents. Moving the migration into the next unpublished minor keeps upgrades safe for sites already past 6.43.
ref #28291

Ghost exports setting values as strings, so the machine payments amount validator needs to accept integer strings during DB import while still rejecting invalid or sub-cent values.
ref #28291

The machinePayments labs flag is intentionally exposed through the config API, so the acceptance snapshot needs to include it.
ref #28291

The new machine payment settings add three defaults and introduce a numeric setting value into shared settings types, so the legacy settings assertion and posts settings helper need to reflect that.
ref #28291

The settings API now includes machine payment settings in browse/edit responses, so the e2e snapshot matcher and stored snapshots need to account for the three new values.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 65.60847% with 260 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.79%. Comparing base (509e457) to head (f59a97f).

Files with missing lines Patch % Lines
...core/frontend/services/machine-payments/service.js 61.13% 95 Missing and 1 partial ⚠️
ghost/core/core/frontend/services/llms/service.js 56.06% 58 Missing ⚠️
...achine-payments/stripe-deposit-address-provider.js 65.78% 52 Missing ⚠️
.../services/machine-payments/adapters/mpp-adapter.js 68.85% 19 Missing ⚠️
ghost/core/core/server/models/settings.js 25.00% 15 Missing ⚠️
...ore/frontend/services/routing/controllers/entry.js 68.29% 13 Missing ⚠️
...services/machine-payments/adapters/x402-adapter.js 89.85% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #28291      +/-   ##
==========================================
+ Coverage   73.63%   73.79%   +0.16%     
==========================================
  Files        1536     1540       +4     
  Lines      130821   131477     +656     
  Branches    15653    15828     +175     
==========================================
+ Hits        96324    97028     +704     
+ Misses      33532    33486      -46     
+ Partials      965      963       -2     
Flag Coverage Δ
admin-tests 54.21% <ø> (+0.02%) ⬆️
e2e-tests 73.79% <65.60%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

migration [pull request] Includes migration for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants