Repository navigation
feat(bidding): add update_ad_group_cpc_bid with a bid precondition guard - #12
Merged
Merged
Conversation
Max CPC lives at AD GROUP level in Bing (CpcBid.Amount) -- there is no
campaign-level max CPC -- so changing "a campaign's max CPC" means one call per
ad group. The server had no bid write at any level, so this was UI-only.
Two guards, because a bid is the highest-blast-radius write here: unlike a
budget, an over-large value has no daily cap to bound the loss.
- expected_current_bid is REQUIRED (not defaulted). The live bid must match it
or the write is refused. A new bid is nearly always computed relative to the
current one ("halve it", "back to $12"), which is exactly the reasoning that
breaks silently when the current one has moved. update_campaign_budget has no
equivalent and will happily write from a stale read.
- The result is read back and asserted before anything reports success.
PartialErrors: [] does NOT mean a write landed on this API -- a $400 budget
write returned exactly that on 2026-08-24 and left the value at $100 for two
days. A mismatch throws and names both values.
assertAdGroupBidPrecondition fails closed on a missing/unparseable live bid;
treating "unknown" as "matches" would make the guard decoration.
Also rejects non-finite, non-positive, and >$500 bids. That ceiling is a
misplaced-decimal catch, not a policy: the highest bid this account has ever
carried is $24.
The tool description warns that under EnhancedCpc (or any auto strategy) this
is a BASE bid Bing adjusts, not a hard ceiling, and that lowering it cuts ad
rank and therefore volume, not just cost.
Tests added: src/mutationRequests.test.ts (builder + guard, 9 cases),
src/writeGate.test.ts (classification, visibility both ways, required arg),
src/tools.contract.test.ts (tool name roster).
RED: 9 failed | 7 passed (both functions undefined)
GREEN: 16 passed
RED: 4 failed | 23 passed (tool unregistered, not in WRITE_TOOLS)
GREEN: 156 passed (1 unrelated pre-existing failure: the shared harness's
npm-audit check times out at 5s, network-dependent)
Verified live against account 141522471, campaign bing_bofu_nonprofit_core:
all 3 ad groups moved 24 -> 10 with read-back confirmation, and a deliberate
replay with the now-stale expected_current_bid: 24 was refused by the guard --
so the guard is confirmed wired to the real call path with real arguments,
not just green in isolation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…out fix 0.1.3 backports the dependency-health fix: the npm-audit test gave its execSync a 30s budget but ran the it() under vitest's 5s default, so a slow-but-healthy audit was killed by the runner. That failed CI here on every ubuntu leg (10.7s) while all three windows legs passed. Stays on the 0.1.x line deliberately. 0.2.x adds a stdout-purity suite whose timeout has the same defect -- a 10s ceiling against 5s of hardcoded internal waits -- and it fails against this server, which does a network auth refresh before connecting its transport. Verified by overlaying the 0.2.x build here: the audit test goes green and stdout-purity goes red, isolated, at 10.32s. Trading one red for another. Local: 157 passed (was 156 passed, 1 failed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The server had no bid write at any level, so max CPC changes were UI-only. This adds one, with the guard
update_campaign_budgetlacks.Max CPC lives at ad group level in Bing (
CpcBid.Amount) — there is no campaign-level max CPC — so changing "a campaign's max CPC" means one call per ad group.Why two guards
A bid is the highest-blast-radius write this server exposes. A budget error is self-limiting (the daily cap bounds the loss); a bid error applies to every click in every auction with no ceiling.
Before —
expected_current_bidis required, not defaulted. The live bid must match or the write is refused. A new bid is nearly always computed relative to the current one ("halve it", "back to $12") — exactly the reasoning that breaks silently when the current one has moved.update_campaign_budgetwill happily write from a stale read.After — the value is read back and asserted.
PartialErrors: []does not mean a write landed on this API: a $400 budget write returned exactly that on 2026-08-24 and left the value at $100 for two days. Nothing reports success here without a re-read.assertAdGroupBidPreconditionfails closed on a missing or unparseable live bid — treating "unknown" as "matches" would make the guard decoration.Also rejects non-finite, non-positive, and >$500 bids. The ceiling is a misplaced-decimal catch, not a policy — the highest bid this account has ever carried is $24.
Enhanced CPC caveat, in the tool description
Under
EnhancedCpc(or any auto strategy)CpcBidis a base bid Bing adjusts up and down, not a hard ceiling — and lowering it reduces ad rank, so it cuts volume as well as cost. The description says so, because "set max CPC to $10" reads like a cap and isn't one.Tests
src/mutationRequests.test.tssrc/writeGate.test.tsBING_ADS_MCP_WRITE, exposed with it,expected_current_bidinrequiredsrc/tools.contract.test.tsOne unrelated pre-existing failure: the shared harness's
npm auditcheck times out at 5s (network-dependent).Verified live, not just green
Account 141522471, campaign
bing_bofu_nonprofit_core. All three ad groups — Donation Software, Nonprofit Software, Nonprofit CRM — moved 24 → 10 with read-back confirmation. Then a deliberate replay carrying the now-staleexpected_current_bid: 24was refused:So the guard is confirmed wired to the real call path with real arguments — not merely passing in isolation.
🤖 Generated with Claude Code