goes to another screengoes to another state of this screenfaded = not mocked yetEverything else — the switch, the ladder, the pickers — works, it just stays on this screen. The URL always names the state you are on.
3active listings on this account1job blocked at push
Money rules
every margin in the listing builder
%A row below this warns amber and still lists.
%Charged by the channel on every sale.
5% + $0.25 per orderSet on the .
Margin is price − product cost − shipping − sales channel fee − ad fee, shipping being postage plus packaging material.
Compliance
read by every content generation on this accountSaved edits reach jobs already open.
House rulesapply to every sales channel. Nothing below can loosen them.
This channel’s rules — added on top of the house rules
Listings on this account
26Read-only. A listing never moves between accounts.
Listing
SKU
Type
Price
State
Listing defaults
copied into a job at its startChanging these changes the next job, never a listing that exists.
Promoted listings
%Applied only when a job promotes the listing.
Operating defaults
synced 2026-08-28 ·
synced 2026-08-28 ·
Return policy has drifted. eBay now calls it Standard returns — 30 day; we hold Standard 30-day returns. Same policy ID.
Ladder models
shared library — a size and its tier are one record
Standard
used by 2 accounts
Preview$
$
Pack
Discount
Pack price
Per unit
Gross margin
Gross margin is price less cost only. It is not the 35% margin floor above — that one is net, and also takes off shipping, the channel fee and the ad rate (D21). Combos take the ladder by bottle count; a size with no tier of its own borrows the deepest tier at or below it, and the listing builder names which.
This account uses Standard.
Identitycopied onto every new job at its start
The last segment of every SKU this store mints — LUCF-3-OB-VD. Changing it does not rename a SKU that already exists, in Shopify or anywhere downstream.
the product’s brandWritten to Shopify’s vendor from the catalog product. Not a setting.
Starting valuesthe job may change these — they are a starting point, not a rule
The store’s live catalogue runs four different shapes — 24 products at 1/3/5, 18 at 1/3, 14 single-variant and 14 at 1/3/6. That is old data never brought forward, not four rules, so this is a default and the job’s Shape rung is a multi-select.
Units is the default because it needs no per-product customisation — a bottle, a jar and a bag are all units. A job that sells one form can say so.
Rounds every value the ladder generates. Without it a percentage ladder emits $20.09 and the operator retypes each tier, which decides whether the generator gets used at all.
Runs indefinitely once set, on packs of 3 or more. It goes inside the SKU’s ShipStation bundle, raising cost and weight and never price. Shopify’s own buy-X-get-Y discount cannot serve it here — see Store behaviour.
Contentcopied into every new job and read-only there
Vital Depot — six sections350–450 words, SEO title ≤ 60, SEO description 140–165, and no phrase reused across a batch.
not extractedThis is the one rule that does not exist yet, and the prompt has no voice clause without it. The writer therefore picks its own register. RI-585.
What a job in flight does with these
Everything on this tab is copied onto a job at its start and frozen there. Editing it now reaches the next job, never J-0121, which holds its own copy. Compliance, above, is the exception: it is read live.
Payment gatewaythe fee rates below hang off this
Set once at store setup and copied onto every job. Never inferred from what neighbouring products use. A job whose store has this unset stops and asks.
The pair is real, not a fallback: 82 of 93 live campaign products carry load balancer 17 and the rest carry gateway 40.
Processing feesper MID — every store has its own · together they are this account’s sales channel fee
% of priceThe processor’s cut. There is no marketplace commission on this channel — this is the gateway’s.
$Flat, once per order — so it lands once on a five-pack, not five times.
% heldHeld, not spent. It comes back, and it is still deducted because the cash is not available in the meantime — the Fees cell in the pricing grid names it separately.
Compare-atwhat the strikethrough price is measured against
MSRP is primary. Compare-at is MSRP × units, so a 3-pack is compared against three bottles at full price.
× one-unit priceUsed for a product with no MSRP, so a missing catalog field never stalls a job.
% offThe saving grows with pack size under the MSRP basis. A row past this threshold is flagged in the grid. Flagged, not refused.
Nothing measured yet
This store has never connected.
Automatic publishinga store setting, not ours — this is a readout
A product must be ACTIVE for CheckoutChamp to sync it, and DRAFT is what stops the sync — so every product in a job sits ACTIVE for the whole build. On this store ACTIVE also auto-publishes to the channels below within seconds, and no API call takes it back off.
The one-time fix is switching automatic publishing off for the first two on the store itself. Until it is done, every job exposes its products to those two channels for the length of the build.
Checkoutmeasured 2026-09-06, not configured here
CheckoutChampThe theme inlines a redirect script on every page: checkout() calls preventDefault() and sends the shopper to secure.vitaldepot.com/checkout. Shopify’s own checkout is never reached.
unreachableA Shopify automatic discount applies on a cart and at checkout, and this store hands off before either finishes, so discountAutomaticBxgywould fire only on the redirect-failure path. This is why a gift lives in the SKU’s bundle and in the copy instead.
Shopify checkoutThe plugin’s one published setting for a failed redirect. It is the only path on which a Shopify-side discount would apply.
What a job does not push to Shopify
House compliance rules
Apply to every sales channel and every listing. A channel’s own rules add to these; none can loosen them. Edit in Settings
Design notes — sales channel account
MR-079 · record half · MR-395 · one screen for every channel · spec references/ebay-planning/SALES-CHANNEL-ACCOUNT.md
MR-401 — compliance is two levels, and the house rules are visible here
House rules apply to every channel and cannot be loosened (Paul, 2026-09-14): never a disease, support verbs only, every benefit traced to our label, the disclaimer. An account’s text only adds, so the samples here no longer repeat a house rule. View house rules opens them read-only, so a person editing a channel’s rules can see what is already covered (RI-804, RI-806). They are edited on Settings › Compliance rules, and the modal shows that same text, from _house-rules.js (RI-807, MR-403).
MR-397 — a Shopify store connects with API keys; empty Compliance is allowed
No OAuth for a Shopify store. Paul: this isn’t a SaaS product — CheckoutChamp and Shopify are reached with API keys kept in .env.local. So CA-01’s card has no authorised or expires step and no Reconnect: it says whether the Shopify key is there, its scopes, and whether a Shopify call and a CheckoutChamp call worked. eBay keeps its OAuth card, because eBay requires a user grant (RI-800). Not drawn: the CheckoutChamp CRM sign-in that three operations still need.
An account with no Compliance text can run jobs — nothing blocks or warns (RI-797).
One account screen for every channel: a shared top, then channel tabs (MR-395 / RI-790 … RI-799)
There were two account screens and they disagreed. This one (MR-079) drew every account, Shopify included, with a 2.9% fee and an empty “no operating defaults” box; sales-channel-account-shopify.html (MR-281) drew CA-01 again with 5% + $0.25 and three tabs of real settings, and had no name field, Active switch, connection card or listings. The list sent every row here, so #acct=CA-01 actually showed CA-02. Paul, 2026-09-14: one account screen, used for every channel.
The top is what every account has: name, channel, active, connection, margin floor, sales channel fee, Compliance and the listings on the account. Below it, tabs the channel has: eBay’s Listing defaults (the ad rate, operating defaults and policies) and Ladder models; Shopify’s New job defaults, Money and Store behaviour, moved here from MR-281 with their values. The hash names all three coordinates — #acct=CA-01&tab=money — and #state= still picks one of the five connection scenarios. sales-channel-account-shopify.html is now a redirect to the matching tab here.
“Final value fee” is now “Sales channel fee”, because on a Shopify store the fee is not eBay’s commission but CheckoutChamp’s processing rates. On a Shopify account the top shows the fee and does not edit it: the three rates are typed once on the Money tab and the top reads them, so two copies of one number cannot disagree. MR-281’s values win — 5% + $0.25 per order, 0% reserve — and the 2.9% is gone. The ad rate is eBay-only (promoted listings has no Shopify counterpart), so it moved into eBay’s Listing defaults.
Compliance is the shared rich text editor over Markdown, per account (RI-784). It is read live, not copied into a job (RI-785) — the one line beside it says so. MR-281’s frozen Content rules › Compliance pack is this section now; Body template and House voice stay in New job defaults, copied at job start. The texts on CA-02, CA-03 and CA-01 are illustrative (SAMPLE-DATA.md); CA-04 has none yet.
“Job settings” is “New job defaults”: they are the account’s values each new job copies at its start, not settings of a job. The Vendor selector is gone.references/shopify-field-map.md already decides Shopify’s vendor is the product’s brand — Google Shopping reads vendor as brand, and the always “Vital Depot” option was the mistake vital-depot-catalog-analysis.md flags on 11 live products. A choice with one right answer is a line, not a control.
Moved out of the page with the Shopify tabs (their one-line consequences stayed at the controls): the load balancer / gateway pair is recorded as a pair because the two are different kinds of value, and a mixed-gateway store or a first product on a new store would make inference produce a plausible answer with no error. The processing rates are stored, not derived — they are terms of a merchant agreement, in no CheckoutChamp field (RI-483, RI-416). The reserve is held, not spent, so a store with one reads a margin lower than it finally earns; the pricing grid names it separately. The MSRP compare-at basis makes the saving grow with pack size — an $80 MSRP against a $30 five-pack per-unit claims 62% off — which is what the inflation alert is for. Shopify’s native buy-X-get-Y gift cannot fire on this store because checkout redirects to CheckoutChamp first, so a gift lives in the SKU’s ShipStation bundle and the copy is the only place a customer learns of it (RI-488). Weight and package details are deliberately not pushed to Shopify: ShipStation owns fulfilment, and a second copy is a value that drifts with nothing reading it. The house voice is still unextracted (RI-585); when it is, it must name its exclusions as well as its exemplars.
The connection is a machine item, so it is a card
MR-066 established the split on catalog-product-setup.html: items waiting on a person are binary and belong in a table; items waiting on a machine run a lifecycle and belong in cards, because a cell cannot hold a lifecycle. This is that card’s second instance, applied to a channel gate rather than a catalog one — the first evidence for or against MR-066’s claim that every later gate screen reuses both the surface and the split.
Four beats: checked (when we last pinged) · authorised (when the grant was made and when it expires) · scopes (granted against an adapter-supplied required set — eBay’s four are not Shopify’s) · proved (a real read-only call came back). The fourth beat is the difference between we hold a token and it works, and it is the one a naive design leaves out.
Five states, one set of nodes
Connected · expiring · scope gap · lost · never connected are rendered from one fixture through one set of DOM nodes, never five copies — so no two states can drift into describing different screens. The harness strip and the URL name which.
scope gap is the state with no precedent. The token is valid and a required scope is missing, so some calls succeed and others fail. It is not lost because the recovery is a re-grant rather than a re-auth, and the failures it causes are selective rather than total.
Reconnecting happens on Settings › Integrations (Paul, 2026-09-15, MR-406, RI-834): the card keeps its status and checks, and its button opens that account’s row there, drawer open. First-auth and reconnect are still one action.Connect on a never-connected account and Reconnect on a lost one start the identical handoff — which is why create (MR-070’s pattern: the record returns, the slow half follows) does not need an OAuth step of its own.
Save and Reconnect are two actions, and they sit apart
The record has editable fields now, so it needs a save — and a save is not a reconnect. One writes fields we own; the other leaves for the provider and comes back with a credential we do not own. A connection can be fixed with nothing to save, and settings can be saved while the connection is still dead, so neither can be a mode of the other.
The split is structural rather than a matter of two buttons being differently worded: record actions live in the page header (Save changes, Discard, and the unsaved-changes chip); connection actions live inside the connection card (Check now, and Reconnect — now a link to Integrations, MR-406). Check now moved out of the header in this round for exactly that reason. Every later screen with both kinds of action should place them the same way.
The active switch is a setting, not an immediate action — it waits for Save like every other field. One screen with two save models is how an operator learns to distrust the save button.
The name is ours; the external ID is not
Name is a free field we own and the list pins as its first column. External account is the provider’s and is read-only here. Account ID is the immutable channel_account_id every listing and command binds to. Three names, three owners, and the header says which is which rather than leaving an operator to find out by trying to edit one.
Two axes, and what a broken one does not do
active is the operator’s switch; connection is a system fact. A lost connection does not flip active off — auto-deactivating would silently change what happens on reconnect. The list carries the contradiction instead, and the blast radius here says what it costs: active listings on the account, and jobs blocked on it. Not orders — those are ShipStation and CheckoutChamp’s.
Money rules, and what they close
Margin floor, sales channel fee (called final value fee until MR-395) and ad rate are the three terms D21’s formula was missing: price − cost − shipping − channel fee − ad fee. MR-064 hard-coded 13% and 2% into a fixture and MR-075 recorded that the floor “has no home”. It has one now. The job still decides whether the ad rate applies (the promoted toggle) — the account only supplies the number.
Ladder models are a shared library, so this is master–detail
It was a dropdown over one editable table, which could not create, rename, duplicate or delete a model — and worse, editing the table silently rewrote whichever model happened to be selected. The section is now a library manager: models on the left with a used-by count, the open model’s tiers on the right.
Every model is library-wide (MR-081). An account picks one; it never owns one. A model therefore carries no owner field — who uses it is derived from the accounts’ own assignment, which is the only way the count stays honest when an account is reassigned. The consequence is that every edit has a blast radius, so the detail pane always says who else moves before you save, and Duplicate is the answer to “I want one number different for one account”.
Opening a model is not assigning it. Clicking a row in the list shows you that model; only Use ‘X’ for this account changes what the account uses. This is the same separation Save and Reconnect have — two gestures that a single control would have quietly merged.
A model in use cannot be deleted, and the button says by whom rather than failing on click.
This settles a conflict rather than creating one. MR-076 says a pack size and its discount tier are one record, so the model owns sizes as well as tiers — which makes MR-012’s “account package set” simply the sizes of the model the account uses. MR-075’s label CA-01 default was a per-account artifact and is gone; the library model is called Standard.
Gross margin is not the margin floor, and must not look like it
The grid carries Pack price, Per unit and Gross margin as separate columns, driven by a one-bottle price and a cost per bottle. Gross margin is (per unit − cost) / per unit — COGS only.
The 35% margin floor sitting a few inches above it is a different measure. D21’s floor is net: price − cost − shipping − channel fee − ad fee. Two percentages on one screen that are not the same measure is a real misread risk, so the hint under the table says which is which, and the column takes no colour for being low. Tinting it under 35% would have invited exactly the comparison the hint warns off; only a negative margin is coloured, because that is unambiguous under any measure.
The price and cost fields are typed or picked: a caret opens a menu of presets, and the field itself takes any number.
A native <datalist> was built first and had to be replaced — it is the wrong primitive. Browsers filter a datalist by what is already in the field, so a box holding 29.95 prefix-matches none of 19.99 / 24.99 / 29.99 and the menu opens empty; the cost box holding 4.50 offered exactly one option, the value already in it. Datalist is a filtering autocomplete, not a preset picker. A preset list has to ignore the typed value, which means owning the menu — about thirty lines, still no dependency, and keyboard-reachable (↓ opens, arrows move, Enter picks, Escape closes).
The example price is a preview, and it found a bug
A discount column of bare percentages does not tell you what the ladder does. The grid now carries an example price computed live from a familiar product and its one-bottle price, using MR-075’s cascade: × 1 = base · × N = base × N × (1 − tier N), with the per-unit figure underneath.
It is a preview and is stored nowhere. MR-075 rung 4 is where a one-bottle price is actually typed, and that belongs to the job. So this control redraws the ladder and is pointedly left out of the dirty check — saving a record must never mean saving a number someone typed to see what a discount does.
Computing it instead of asserting it exposed a conflict in the canonical fixture. SAMPLE-DATA states the MEMP 2-pack ladder seed as $56.98, but 2 × 29.95 − 5% is $56.90 — 8¢ apart. Either the seed is an arithmetic slip, or prices are charm-rounded to .98, which would be a real pricing rule that no decision, spec or screen states anywhere. The screen shows the raw figure and does not round. Flagged, not guessed.
Seeds are cached, not fetched
Shipping and return policies are referenced, never created per listing (MR-025), and the reference list is held on our side per account. eBay business policies change rarely, so a disconnected account stays fully readable and editable — the connection gates the Refresh, nothing else. A periodic re-sync compares ours against the provider’s and disagreement is drift → correct or accept, the pattern SAMPLE-DATA already exercises, rather than a state invented here.
A pack size and its discount tier are one record (MR-076), so adding a size asks for both. Editing a seed never reaches an existing listing or a running job.
Decision trail
MR-025 — operating defaults on the channel account. This is their editor; nothing wrote them before.
MR-026 / MR-012 / MR-076 — the ladder, the package set, and the one-record rule.
MR-064 / D21 — the margin basis. Its three missing terms live here.
MR-066 — the machine-item card, reused rather than redrawn.
MR-070 — create returns and the slow half follows.
MR-075 · reversed — the floor was to be one channel-level number every account inherits. It is an account field instead.
OPEN-ITEMS P1 · closed — the floor has an owner and an editor.
MR-281 · merged — the Shopify account screen’s three sections are this screen’s Shopify tabs (MR-395).
MR-395 — one screen, shared top, channel tabs; Sales channel fee; Compliance; no Vendor selector.
Open — needs Paul
1 · Three eBay accounts can hold three different floors. That is what dropping the channel record costs, and nothing on this screen notices the drift. Taken knowingly; if it bites, the fix is a channel record added later with accounts inheriting, not a rule bolted on here.
2 · The expiry threshold is a guess.expiring lights at 14 days. eBay user tokens run about 18 months, so 14 days is roughly “you see this twice before it matters” — but nothing justifies the number, and it is the only figure on the screen that nothing does.
3 · Should the preview pull the real cost? The cost field defaults to $4.50 as asked, but SAMPLE-DATA holds Memopryl’s actual cost/unit as $3.85, owned by QuickBooks and read-only everywhere else. Choosing a product sets the price and leaves cost alone, which is deliberate for a preview — but if the point is to see a real margin, the real cost is the one to show, and then the field is a read-only fact rather than a knob.
4 · Is $56.98 a rounding rule or a typo? The example column computes $56.90 where SAMPLE-DATA asserts $56.98 for the same 2-pack. If prices are charm-rounded to .98 that rule belongs on the sales channel beside the margin floor, and it changes every price the listing builder derives. If it is a slip, the fixture needs correcting. Nothing anywhere states which.
5 · Where does the model library really live? MR-077 deleted package-builder.html because “two screens that can each define a job’s packages is how they drift”, and Settings › Package sets is a named, still-stubbed nav item. The manager is built here because this is where the ladder is used, but it should be one editor with two doorways — if Settings grows its own, that is the drift MR-077 punished.
6 · Can a job start on an account with no Compliance text? CA-04 has none. Content generation reads it live (RI-785), so an empty one means generation runs with no channel rules at all. Nothing on this screen blocks it (RI-797).
7 · A Shopify store has a second connection this card does not draw. MR-281’s header said CRM session healthy — the CheckoutChamp session the job signs in with. The card here is the Shopify grant only; whether the CheckoutChamp session belongs on it as a fifth beat or a card of its own is not decided.
8 · Carried from MR-281: whether CheckoutChamp’s own free-gift mechanism reaches a product synced through the plugin; whether any CheckoutChamp endpoint exposes the fee rates; and whether shipping stays a cost we absorb — Paul, 2026-09-08: “Sometimes we will charge shipping.”