Background
Spun out of #2481 review (thanks @bokelley). The hotfix in #2481 makes getPriceByLookupKey resolve unambiguous aliases like explorer_annual → aao_membership_explorer_50, and refuse ambiguous ones. That's a band-aid — root cause is that Addie's LLM invents lookup keys from tier name + interval instead of passing the canonical lookup_key returned by find_membership_products.
Every silent server-side remap is a missed chance to teach the model. With the refusal-on-ambiguity guard in #2481, the resolver only helps in the unambiguous case (today: just explorer_50). As soon as anyone adds a monthly Explorer SKU, explorer_annual will collide → undefined → "No price found" error. Safe behavior, but will look like a regression to whoever adds that SKU.
What to do
1. Surface did_you_mean in the tool response
Mirror the pattern from PR #2482. When create_payment_link / send_invoice / confirm_send_invoice (in server/src/addie/mcp/billing-tools.ts) get a non-canonical lookup_key:
- If
getPriceByLookupKey resolved an alias, include the canonical key in the response so the LLM sees "you asked for explorer_annual; canonical is aao_membership_explorer_50 — use that next time."
- If it didn't resolve (ambiguous or unknown), the existing "No price found. Available: ..." error already gives the LLM the canonical options.
The point: make the canonical key visible to the model so subsequent calls in the same conversation use it verbatim.
2. Tighten tool descriptions
In server/src/addie/mcp/billing-tools.ts, update the description fields on create_payment_link, send_invoice, confirm_send_invoice so the model is explicitly told:
Pass lookup_key verbatim from the find_membership_products response. Do not construct it from the tier name and billing interval (e.g. do not pass explorer_annual — pass aao_membership_explorer_50).
Today's descriptions just say "The product lookup key from find_membership_products" — that's what the LLM is already (mis-)interpreting.
3. Preserve interval signal as a tiebreaker (optional)
When/if the catalog gains annual+monthly pairs for a tier, resolveLookupKeyAlias could keep the stripped suffix (annual / monthly) and prefer a price whose recurring.interval matches. Today this is theoretical, but worth wiring up before the catalog change happens.
Acceptance
- LLM receives canonical
lookup_key in create_payment_link response when it passed an alias.
- Tool descriptions explicitly forbid constructing the key.
- The TODO in
resolveLookupKeyAlias (server/src/billing/stripe-client.ts) pointing at this issue can be removed.
References
Background
Spun out of #2481 review (thanks @bokelley). The hotfix in #2481 makes
getPriceByLookupKeyresolve unambiguous aliases likeexplorer_annual→aao_membership_explorer_50, and refuse ambiguous ones. That's a band-aid — root cause is that Addie's LLM invents lookup keys from tier name + interval instead of passing the canonicallookup_keyreturned byfind_membership_products.Every silent server-side remap is a missed chance to teach the model. With the refusal-on-ambiguity guard in #2481, the resolver only helps in the unambiguous case (today: just
explorer_50). As soon as anyone adds a monthly Explorer SKU,explorer_annualwill collide →undefined→ "No price found" error. Safe behavior, but will look like a regression to whoever adds that SKU.What to do
1. Surface
did_you_meanin the tool responseMirror the pattern from PR #2482. When
create_payment_link/send_invoice/confirm_send_invoice(inserver/src/addie/mcp/billing-tools.ts) get a non-canonicallookup_key:getPriceByLookupKeyresolved an alias, include the canonical key in the response so the LLM sees "you asked forexplorer_annual; canonical isaao_membership_explorer_50— use that next time."The point: make the canonical key visible to the model so subsequent calls in the same conversation use it verbatim.
2. Tighten tool descriptions
In
server/src/addie/mcp/billing-tools.ts, update thedescriptionfields oncreate_payment_link,send_invoice,confirm_send_invoiceso the model is explicitly told:Today's descriptions just say "The product lookup key from find_membership_products" — that's what the LLM is already (mis-)interpreting.
3. Preserve interval signal as a tiebreaker (optional)
When/if the catalog gains annual+monthly pairs for a tier,
resolveLookupKeyAliascould keep the stripped suffix (annual/monthly) and prefer a price whoserecurring.intervalmatches. Today this is theoretical, but worth wiring up before the catalog change happens.Acceptance
lookup_keyincreate_payment_linkresponse when it passed an alias.resolveLookupKeyAlias(server/src/billing/stripe-client.ts) pointing at this issue can be removed.References