Skip to content

feat(mcp): a ChatGPT connector URL that neither sells nor promotes selling - #201

Merged
milstan merged 1 commit into
mainfrom
milstan/chatgpt-topup-gating
Sep 2, 2026
Merged

feat(mcp): a ChatGPT connector URL that neither sells nor promotes selling#201
milstan merged 1 commit into
mainfrom
milstan/chatgpt-topup-gating

Conversation

@milstan

@milstan milstan commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The OpenAI app directory rejects an app that sells digital goods and separately forbids promoting the purchase; Anthropic's Software Directory Policy has no equivalent clause, so Claude keeps the top-up flow untouched. POST /chatgpt/mcp is the URL we submit to OpenAI: same Hono app, same auth, same handler, built with includeCommerce: false. Removing only the two Stripe tools was not enough, because three separate places still pushed a top-up the agent could no longer produce, so four things now go away together: the tools are unregistered, tool descriptions lose their {{commerce}} blocks, the QUOTA_TOPUP instruction paragraph is not pushed, and LeadbayClient.commerce drops the two selling sentences from the QUOTA_EXCEEDED hint. Nothing is reworded for ChatGPT: the new promptforge {{commerce}} marker only ever deletes, so there is no softened second wording to drift, and commerce-gate.test.ts asserts the gated strings are character-level subsequences of the Claude ones (any changed character fails). A path rather than a clientInfo sniff or a ?commerce=off query, because the OAuth protected-resource identifier is the path, so a query flag would leave both URLs advertising the same resource and a client reconnecting to its registered audience would get the selling tools back.

Verified against production (real token, real dist/http-server.js on a socket, real Leadbay API):

/mcp           tools=60  instructions=24128c  all 6 selling phrases PRESENT  create_topup_link -> live cs_live_ URL
/chatgpt/mcp   tools=58  instructions=22625c  all 6 absent                   create_topup_link -> Unknown Leadbay tool

removed tools                              : [create_topup_link, open_billing_portal]
instructions is a pure DELETION of Claude's: true (24128 -> 22625)
descriptions all pure deletions            : true
descriptions actually gated                : [account_status, scan_portfolio_signals]

The generated descriptions were also diffed against origin/main to confirm the default rendering did not move by a byte. pnpm -r test 2269 passing, pnpm -r typecheck clean. Version 0.34.0 (minor: new endpoint), and the installer's ChatGPT Desktop entry now hands out HOSTED_MCP_URL_CHATGPT.

One judgment call worth a veto. In scan_portfolio_signals the top-up offer was welded mid-sentence into "On a 429 mid-scan, partial matched is returned with quota_exceeded: true — offer the user…". Deleting just the clause would have meant rewording the surrounding prose, so I gated the whole paragraph; ChatGPT is therefore not told that partial results come back on a 429, though quota_exceeded is in the output schema regardless.

Known and accepted: adding a third selling tool means adding its name to COMMERCE_TOOL_NAMES by hand. Nothing catches that automatically; a keyword guard was tried and is too noisy to be useful, so both names sit behind one constant with the OpenAI policy quoted directly above it.

🤖 Generated with Claude Code

…lling

The OpenAI app directory rejects an app that sells digital goods — "plugins may
conduct commerce only for physical goods. Selling digital products or services
— including subscriptions, digital content, tokens, or credits — is not
allowed" — and separately forbids promoting the purchase: a plugin "must not
display subscription plans, initiate new subscriptions, or promote upgrades" or
"link directly to a checkout". Signing in to an existing paid account and using
what it entitles you to IS allowed. Anthropic's Software Directory Policy has
no equivalent clause, so Claude keeps the top-up flow untouched.

POST /chatgpt/mcp is the URL submitted to OpenAI. Same Hono app, same auth,
same handler, built with includeCommerce: false. Four things go away together —
the tools, and every text that would promote buying them, since removing only
the tools left the agent still pushing a top-up it could no longer produce:

  1. leadbay_create_topup_link / leadbay_open_billing_portal unregistered.
  2. Tool descriptions lose their {{commerce}} blocks
     (NO_COMMERCE_TOOL_DESCRIPTIONS, emitted by promptforge).
  3. The QUOTA_TOPUP instruction paragraph is not pushed. quota-topup.md was
     split verbatim; the neutral half became quota-refresh.md, pushed on both.
  4. LeadbayClient.commerce drops the two selling sentences from the
     QUOTA_EXCEEDED hint. One client per session, so it cannot leak.

Nothing is reworded for ChatGPT. {{commerce}} only ever DELETES, so there is no
softened second wording to drift. Enforced two ways: commerce-gate.test.ts
asserts the gated strings are character-level subsequences of the Claude ones
(any reworded character fails), and the generated descriptions were diffed
against main to confirm the default rendering did not move by a byte.

A path, not a clientInfo sniff and not a query parameter: the OAuth
protected-resource identifier IS the path, so ?commerce=off would leave both
URLs advertising the same resource and a client reconnecting to its registered
audience would get the selling tools back.

Verified against production — real token, real dist bundle on a socket:
/mcp lists 60 tools, a 24,128-char instruction string carrying every selling
phrase, and returns a live cs_live_ checkout URL. /chatgpt/mcp lists 58, errors
on the tool, carries 22,625 chars with none of those phrases, and both its
instructions and its descriptions are pure deletions of Claude's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T22:41:49.682634Z 18014ac PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18014ac0fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +666 to +668
const noCommerce = includeCommerce
? undefined
: NO_COMMERCE_TOOL_DESCRIPTIONS[t.name];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate commerce text in all tool metadata

When /chatgpt/mcp lists tools, this fallback retains the original description for every tool absent from NO_COMMERCE_TOOL_DESCRIPTIONS, which currently contains only leadbay_account_status and leadbay_scan_portfolio_signals. Consequently, exposed tools such as leadbay_extend_lens, leadbay_enrich_titles, and leadbay_bulk_enrich_status still instruct the agent to offer an upgrade or a wait-or-top-up choice; unchanged output schemas leak similar guidance, including scan-portfolio-signals.ts:241. This defeats the commerce-free endpoint during ordinary tool discovery, so all agent-visible tool metadata needs commerce gating rather than falling back unchanged.

Useful? React with 👍 / 👎.

Comment on lines +657 to +658
const includeCommerce = opts.includeCommerce !== false;
client.commerce = includeCommerce;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Suppress commerce hints in composite results

When a ChatGPT session encounters quota or billing failures, setting client.commerce only alters the generic mapErrorResponse hint; several exposed composites construct their own selling text. For example, bulk-enrich-status.ts:352-356 and qualify-status.ts:338-340 direct the agent to the removed top-up tool, extend-lens.ts:357-360 offers a plan upgrade, and import-leads.ts:1787-1792 links directly to billing. These strings still reach the agent from /chatgpt/mcp, so the commerce mode must also govern tool-specific result and error envelopes.

Useful? React with 👍 / 👎.

// For composite-file tools (COMPOSITE_FILE_TOOL_NAMES) the field is also
// declared as required + uses the stronger MANDATORY description; the
// dispatch handler enforces presence by rejecting LAST_PROMPT_REQUIRED.
const includeCommerce = opts.includeCommerce !== false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate commerce-bearing prompts

If a client invokes MCP prompts on /chatgpt/mcp, includeCommerce has no effect on prompts/list or prompts/get, so the unchanged leadbay_getting_started body still tells the agent that quota guidance covers the “wait-vs-top-up” choice (leadbay_getting_started.md.tmpl:192-194). Prompts become direct agent input and the endpoint advertises the prompts capability, so affected prompts need a commerce-free rendering or must be omitted on this path.

Useful? React with 👍 / 👎.

Comment on lines +34 to +35
export function hasCommerceMarkers(body: string): boolean {
return MARKER.test(body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make commerce marker detection stateless

Because MARKER has the global flag, calling .test() advances its shared lastIndex. Repeated renderCommerce(body, "without") calls on the same marked body therefore alternate through the opening and closing tags and can eventually return the original body with literal commerce markers and content intact; calls on a shorter subsequent body can fail similarly. Use a non-global predicate or reset lastIndex before testing so this exported renderer is deterministic.

Useful? React with 👍 / 👎.

Comment thread CHANGELOG.md
Comment on lines +15 to +16
- **If ChatGPT was already set up for you**, our installer now points it at the
new address on its own.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Correct the automatic ChatGPT migration claim

For an existing ChatGPT connector, the installer does not update its configured URL: both installer implementations handle chatgpt-desktop by returning an ok result whose message explicitly says “manual setup required,” without writing any configuration. The existing connector therefore remains on /mcp and continues receiving the commerce-enabled catalog until the user manually replaces it, contrary to this release note; document the required manual migration or implement an actual update path.

Useful? React with 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clean implementation — the {{commerce}} marker is deletion-only, the path-based gate (/chatgpt/mcp) flows correctly through RESOURCE_PATHS/OAuth discovery/CORS, and buildServer's tool filtering + description swap + QUOTA_TOPUP gating are all keyed off the actual exposed-tool set rather than the raw flag. Traced the deleteBlocks blank-line-collapsing logic and the inline-span deletions against the generated NO_COMMERCE_TOOL_DESCRIPTIONS output — no stray blank lines, no leaked markers, no double punctuation. Test coverage (subsequence assertions, real-socket catalog diff) is strong. No high-confidence bugs found; no CLAUDE.md review-guideline violations (no new tool added, so no WORKFLOWS.md row needed).

@milstan
milstan merged commit fd93ddd into main Sep 2, 2026
2 checks passed
milstan added a commit that referenced this pull request Sep 2, 2026
One line: the notifications-inbox snippet now says notification_id inside
account_status, whose template main changed in #201.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant