原始内容
name: drivethru-adidas-click
description: Browser-driven adidas Click B2B toolkit — places purchase orders and runs live inventory / wholesale-pricing checks on the adidas Click portal with Playwright, since adidas exposes no public ordering API. Ordering accepts style number, color, size, quantity, a purchase-order number, and a ship-to address, drives the cart/checkout, and (on confirm) submits the order. Checks reuse the same flow but never buy — inventory reads product pages (no cart), pricing fills a throwaway "DO NOT BUY" cart, reads the priced checkout, and deletes it. Use whenever the user needs to place/draft a PO on adidas Click, or check live stock levels or wholesale pricing.
version: 0.6.0
emoji: 👟
homepage: https://b2bportal.adidas-group.com
metadata:
openclaw:
requires:
bins: [python3]
envVars:
ADIDAS_CLICK_USERNAME:
required: false
description: >
adidas Click B2B portal username (typically an email). Optional —
may instead be passed inline in a tool's stdin JSON. Treat as a secret.
ADIDAS_CLICK_PASSWORD:
required: false
description: adidas Click portal password. Optional env cache; treat as a secret.
ADIDAS_CLICK_BASE_URL:
required: false
description: >
Override for the adidas Click base URL. Defaults to
https://b2bportal.adidas-group.com. Set this if the account uses a
region-specific host.
ADIDAS_CLICK_HEADLESS:
required: false
description: >
Force headless when set to a truthy value (true/1/yes/on).
Default is HEADED, because adidas's Akamai Bot Manager stalls headless
Chromium. On Windows/macOS a headed browser uses the native display, so
nothing extra is needed. Only on a Linux host with no display server
(e.g. a headless container) does the skill auto-start an Xvfb virtual
display (requiring the xvfb system package); Windows/macOS never need
it. Headless will most likely time out at the adidas login page.
ADIDAS_CLICK_USER_AGENT:
required: false
description: >
Override the browser User-Agent. Defaults to a current desktop-Chrome
UA so adidas's Akamai edge serves the automated browser instead of
stalling the response. Only change it if the default UA gets blocked.
install:
uv:
# Browser automation is the only surface. This installs the Playwright
# package; the Chromium binary (a separate ~150 MB download that pip
# can't fetch, and OpenClaw has no post-install hook for) is installed
# automatically on the first run — see the browser-binary note below.
- playwright>=1.40
adidas Click B2B toolkit
adidas Click is a B2B ordering portal with no public API, so this skill places purchase orders by driving the portal with Playwright. Every tool is reached through one CLI entrypoint:
echo '<json-args>' | python3 scripts/adidas.py <action>
The action is the first CLI argument; arguments are a JSON object on
stdin. Each call prints a single JSON object on stdout, or
{"error": {"type": ..., "message": ...}} with a non-zero exit code on failure.
Playwright is imported lazily, so the skill loads without it. The Playwright
package is a declared uv dependency; its Chromium browser binary (a
separate ~150 MB download that pip/uv doesn't fetch, and for which OpenClaw has
no install-time hook) is installed automatically on the first run if missing
— so no manual python -m playwright install chromium is needed. That first run
therefore includes a one-time browser download. If the auto-install fails (e.g.
no network, or missing system libraries), the tool returns a clear
config_error pointing at the manual command.
Build status — full order flow implemented. The skill drives a purchase order end to end: login → add line quantities to the active cart → checkout (Customer PO, delivery location — default / saved / one-time dropship — and shipping method) → Next → Calc. Net Price (waits for the "Done!" tell so the wholesale discount always applies) → Order Now → confirmation number parsed from the redirect URL.
confirm: falseis a full dry run;confirm: trueplaces a real order (no sandbox). The assembled flow has been built and unit-checked selector-by-selector but not yet run against the live site in one pass — watch the first real end-to-end run. Seereferences/order_flow_notes.mdfor the step map and captured selectors.Inventory / pricing checks (
check-inventory-pricing) reuse the same captured steps — product navigation + size read (inventory) and, for pricing, the throwaway-cart → checkout → Calc. Net Price walk — but stop before Order Now and delete the cart. No new selectors: it composes the existing, live-validated ordering steps, so its browser surface rides on the same capture. See the "Inventory & pricing checks" section below.
When to use this skill
Reach for create-purchase-order when the request involves placing (or
drafting) a purchase order on adidas Click: given a style number, color,
size, quantity, a PO number, and a ship-to address, put the order into the
portal cart/checkout and — on explicit confirmation — submit it.
Reach for check-inventory-pricing when the request is to look up live stock
levels and/or wholesale (net) pricing on adidas Click without buying
anything — e.g. "how much JW4306 is in stock in L?" or "what's our
wholesale cost on 24× JW4306 M?". It reuses the ordering flow but never places
an order (see below).
Do not use it for other footwear/apparel vendors, and never invent portal
selectors from prose — the flow lives in the captured
references/order_flow_notes.md.
Actions
| Action | Risk | stdin JSON (key fields) |
|---|---|---|
create-purchase-order |
high — portal write (browser) | {purchase_order: {po_number, lines: [{style, size, quantity}], ship_to?, delivery_location_id?, ship_method?, on_insufficient_stock?, spread_delivery?}, confirm} |
check-inventory-pricing |
medium — read-only intent, but a pricing check briefly creates then deletes a throwaway cart (browser) | {check: "inventory"|"pricing"|"both", lines: [{style, size?, quantity?}], po_number?} |
Run python3 scripts/adidas.py with no action to print the action list.
create-purchase-order input
The order can be passed nested under purchase_order or as top-level fields:
{
"purchase_order": {
"po_number": "PO-12345",
"lines": [
{"style": "JW4306", "size": "M", "quantity": 24},
{"style": "JW4306", "size": "L", "quantity": 12}
],
"ship_to": null,
"delivery_location_id": null,
"ship_method": null,
"spread_delivery": false,
"notes": null
},
"confirm": false
}
Each line is {style, size, quantity}. color is optional — adidas article
numbers encode the color (e.g. JW4306 = the black colorway), so navigating to
the article lands on the right color with no picker.
Delivery location precedence: delivery_location_id (pick a saved location) >
ship_to (add a one-time / dropship location) > default (leave the cart's
preset). A one-time ship_to.state accepts a 2-letter abbreviation or the full
name. ship_method is left at the cart default unless set, and accepts a FedEx
service code (FDGP, FEDE, FED4, …) or its exact label ("FedEx Ground").
po_number is capped at 18 characters and may contain only letters, numbers,
space, and / _ . ? &; any other character (e.g. a hyphen) is auto-replaced with
_ and the substitution is reported in the result's warnings.
By default (new_cart: true) the run creates its own fresh cart (named with
the PO) right after login and makes it active, so on a shared account it never
adds to — or checks out — a teammate's cart. If a cart with the same PO name
already exists (e.g. a leftover from an errored prior run) it is deleted
first so the run never piles onto stale quantities — only carts with that exact
PO name are removed, and any deletion is noted in warnings. Pass
new_cart: false to use the current active cart instead.
Pass screenshot_path to capture the filled page for review.
Out-of-stock handling (read this)
Before entering quantities, the tool reads each size's availability (scrolling
the horizontal size row as needed to load off-screen sizes). A size is either
backorderable (stock below the requested quantity, incl. 0 with a restock
date) or not available (the "X" cell — will never be restocked, cannot be
ordered at all; it can only be removed or substituted). When a line's full
quantity is not available, behavior depends on on_insufficient_stock:
pause(default) — nothing is ordered (even withconfirm: true). The result comes backstatus: "needs_confirmation"with anout_of_stocklist ({style, size, requested, available}) and amessage. The agent must stop and confirm with the user, then re-run.order— order the short line(s) anyway, accepting delayed delivery (adidas spreads the backordered portion over future dates).skip— remove the out-of-stock line(s) and order the rest.
Agent guidance: if the user's request pre-authorizes a choice, set the flag
and skip the pause — e.g. "go ahead and order anything out of stock" →
on_insufficient_stock: "order"; "remove any out-of-stock items without
asking" → on_insufficient_stock: "skip". Otherwise leave it at pause; on a
needs_confirmation result, message the user with the out_of_stock details
and the three choices, wait for their answer, then resume:
- Order them anyway (delayed delivery) → re-run with
on_insufficient_stock: "order". - Don't order them → re-run with
on_insufficient_stock: "skip". - Substitute (e.g. a different size/style so items match) → edit the
linesaccordingly and re-run (optionally with a policy for any still-short substitutes).
spread_delivery is the lower-level knob behind order: on a per-line spread
prompt, false (default) declines (single delivery), true accepts.
The result reports total_quantity (summed pieces, on dry runs too) and, on a
placed order (confirm: true), each line's net line_total and unit_price
(net ÷ quantity) plus the order order_total (net wholesale), read from the
priced review page.
Inventory & pricing checks (check-inventory-pricing)
A read-only lookup that reuses the ordering flow but never places an
order. Pick the mode with check:
check |
What it does | Cart? |
|---|---|---|
inventory |
Reads each line's live size-tile stock level straight off the product page. | No cart created — inventory needs no add-to-cart. |
pricing |
Fills a throwaway cart, advances to the priced checkout screen (the only place wholesale net pricing shows), runs Calc. Net Price, reads the net unit/line prices + order total, then deletes the cart. | Yes — created, priced, deleted. |
both (default) |
pricing plus the inventory levels read while filling the cart. |
Yes — same as pricing. |
{
"check": "both",
"lines": [
{"style": "JW4306", "size": "M", "quantity": 24},
{"style": "JW4306", "size": "L"}
]
}
Each line is {style, size?, quantity?}:
sizeoptional (inventory only): omit it (or pass"*"/"all") to report every size of that style. For a pricing line a specificsizeis needed (a price is per size).quantityoptional: defaults to1. It only affects a pricing line's line total (unit price is the same regardless); an inventory read reports the raw level independent of quantity.
Why a cart at all for pricing? adidas only reveals the discounted wholesale net price on the final checkout screen, after Calc. Net Price runs — there is no price API and no price on the product page beyond a "from" figure. So a pricing check has to fill the cart and walk to that screen, exactly like an order, then stop before Order Now and delete the cart.
The "DO NOT BUY" marker. A pricing check names its throwaway cart and the
Customer PO with a DO NOT BUY {random} marker (e.g. DO NOT BUY 7F3K9) so that
if a run dies mid-flight, the leftover cart is unmistakably safe. The full
"AUTOMATED CHECK - DO NOT PURCHASE - …" wording does not fit: adidas hard-
caps the Customer PO at 18 characters and allows only letters, numbers,
space, and / _ . ? &, so this is the clearest imperative that still leaves room
for a 5-char random suffix (cart names must be unique). Override the marker with
po_number (still ≤18 chars, same charset).
Never pauses on stock. Because a check never buys and deletes its cart, it
does not stop on short/out-of-stock lines: a pause policy is upgraded to
order so every orderable line still gets priced. Sizes that are flatly not
available (the portal's "X" cell) are reported with no price.
Result (status: "checked"): a lines list of
{style, size, color, requested_quantity, available, available_count, status ("in_stock"|"backorder"|"unavailable"), in_stock, unit_price, line_total, note},
plus order_total (summed net, pricing only), total_quantity, po_number (the
marker used, null for inventory-only), and cart_deleted (true/false once a
pricing check tried to remove its cart; null when no cart was made). If the
throwaway cart could not be auto-deleted (e.g. it was the account's only cart), a
warning says so and names it — remove it in the portal.
Credentials
The adidas Click portal login can be supplied three ways (later wins):
Environment variables (preferred for a deployed agent):
ADIDAS_CLICK_USERNAME=... ADIDAS_CLICK_PASSWORD=... ADIDAS_CLICK_BASE_URL=https://b2bportal.adidas-group.com # override for region hostInline in the stdin JSON — pass
username,password, andbase_urlalongside the tool's own arguments.CLI flags —
--username/--password/--base-urlafter the action. Handy for local runs with no env vars and no secrets in the order JSON:echo '{"purchase_order": {...}, "confirm": false}' \ | python3 scripts/adidas.py create-purchase-order --username "$U" --password "$P"Command-line arguments are visible to other processes (
ps) and your shell history — for sensitive/automated use, prefer env vars or the stdin JSON.
If no credentials are present via any of these, the tool exits with
{"error": {"type": "config_error", ...}} (exit code 2) — treat that as a
signal to ask the user for the missing fields. Never guess or reuse credentials
across accounts, or paste secrets the user did not provide.
Write safety
create-purchase-order is the only order-placing action.
- It requires
"confirm": trueto place the order. Without it, it logs in, fills the cart + checkout, and returns a dry-run preview without submitting. Confirm with the user before placing. - Placing an order on adidas Click is a real purchase — there is no
sandbox. Only run
confirm: truefor an order the user actually intends to place.
check-inventory-pricing never places an order — there is no confirm
path. inventory mode touches no cart at all. pricing/both create a
throwaway DO NOT BUY … cart only to reach the priced checkout screen, stop
before Order Now, and delete the cart on the way out. It does mutate
portal state briefly (the cart is created and removed), so it is not purely
read-only, but it cannot buy anything.
Reachability & bot mitigation
adidas Click sits behind Akamai Bot Manager, which fingerprints the client
and stalls the HTTP response (rather than cleanly refusing it) for traffic
that looks automated. The tell is a navigation that hangs to a timeout
(Page.goto: Timeout … exceeded) even though the network path is healthy — a
browser-UA curl to the same host returns 200 in well under a second, while
a bare curl or a default headless Chromium (UA contains HeadlessChrome,
navigator.webdriver=true) gets tarpitted.
A spoofed User-Agent alone is not enough: a headless Chromium is still
fingerprinted (TLS/JA3, HeadlessChrome internals, no GPU/canvas) and tarpitted
even with a real UA. The reliable path is a headed browser on a virtual
display, which presents an ordinary browser fingerprint. So the skill:
- runs headed by default (set
ADIDAS_CLICK_HEADLESS=trueto force headless, which will most likely time out at the login page); - launches Chromium with a real desktop-Chrome User-Agent, a normal
viewport/locale,
--disable-blink-features=AutomationControlled, anavigator.webdrivermask, and a longer navigation timeout; - when headed on a Linux host with no display server, auto-starts an Xvfb
virtual display (spawned directly, so no
xauthneeded) and points$DISPLAYat it — this requires thexvfbsystem package in the image (apt-get install -y xvfb). This is Linux-only: the skill first checks the platform, so on Windows/macOS (native GUI) and on a Linux host that already has an X11/Wayland display, it launches headed directly and never requires Xvfb.
Overrides: ADIDAS_CLICK_USER_AGENT if the default UA ages out. If headed on a
virtual display still stalls, the block is at the egress IP (a datacenter
IP Akamai distrusts) — route through an IP adidas allowlists, the same way
drivethru-sanmar documents needing its caller IP allowlisted. No browser/code
change fixes an IP-reputation block.
Error model
Failures print {"error": {...}} and exit non-zero:
config_error(exit 2) — missing/invalid credentials or an un-captured step.api_error— the portal rejected an action or a page did not match expectations. Includessurface,operation,retryable.connection_error— network / browser-launch / navigation failure (retryable: true).validation_error— bad input JSON or a missing required field.usage/unknown_action(exit 2) — bad CLI invocation; the message lists the validactions.
Surface the human-readable message to the user. Do not retry on
config_error or validation_error.
References
references/order_flow_notes.md— the reverse-engineered portal ordering flow (step map, captured selectors, and the capture checklist for the next step). Start here to continue the build.