Added agent payments for premium markdown URLs#28291
Conversation
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.
|
It looks like this PR contains a migration 👀 General requirements
Schema changes
Data changes
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
WalkthroughAdds 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
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
ghost/core/package.json (1)
186-186: ⚡ Quick winPin
honoto 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 winReplace 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (36)
apps/admin-x-framework/src/api/settings.tsapps/admin-x-framework/src/test/msw-utils.tsapps/admin-x-framework/src/test/responses/settings.jsonapps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsxapps/admin-x-settings/src/components/settings/general/seo-meta.tsxapps/admin-x-settings/src/components/settings/membership/membership-settings.tsxapps/admin-x-settings/src/components/settings/membership/tiers.tsxapps/admin-x-settings/test/acceptance/general/seometa.test.tsapps/admin-x-settings/test/acceptance/membership/tiers.test.tsghost/core/core/frontend/helpers/ghost_head.jsghost/core/core/frontend/services/llms/markdown.jsghost/core/core/frontend/services/llms/service.jsghost/core/core/frontend/services/machine-payments/adapters/mpp-adapter.jsghost/core/core/frontend/services/machine-payments/adapters/x402-adapter.jsghost/core/core/frontend/services/machine-payments/service.jsghost/core/core/frontend/services/machine-payments/stripe-deposit-address-provider.jsghost/core/core/frontend/services/routing/controllers/entry.jsghost/core/core/server/api/endpoints/utils/serializers/input/settings.jsghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.jsghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.jsghost/core/core/server/data/migrations/versions/6.31/2026-05-14-18-06-40-add-machine-payments-settings.jsghost/core/core/server/data/schema/default-settings/default-settings.jsonghost/core/core/server/models/settings.jsghost/core/core/shared/config/defaults.jsonghost/core/core/shared/labs.jsghost/core/package.jsonghost/core/test/e2e-frontend/llms.test.jsghost/core/test/unit/frontend/helpers/ghost-head.test.jsghost/core/test/unit/frontend/services/llms/service.test.jsghost/core/test/unit/frontend/services/machine-payments/adapters.test.jsghost/core/test/unit/frontend/services/machine-payments/service.test.jsghost/core/test/unit/frontend/services/machine-payments/stripe-deposit-address-provider.test.jsghost/core/test/unit/frontend/services/routing/controllers/entry.test.jsghost/core/test/unit/server/data/schema/integrity.test.jsghost/core/test/unit/server/models/settings.test.jsghost/core/test/utils/fixtures/default-settings.json
💤 Files with no reviewable changes (1)
- apps/admin-x-settings/src/components/settings/general/seo-meta.tsx
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 Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Adds x402 and MPP support for paid-members-only markdown URLs so AI agents can pay per post when machine payments are enabled.
machinePaymentslabs flag and the existingllms.txtsetting/llms.txtonly when agent payments are enabled.mdURLs through machine-payment challenges using the existing Stripe Connect patternsllms.txtis off