Install
openclaw skills install @zmtucker/drivethru-adidas-clickBrowser-driven adidas Click B2B toolkit — places purchase orders, runs live inventory / wholesale-pricing checks, and pulls shipment tracking numbers from the adidas Click portal with Playwright, since adidas exposes no public 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) and reports the **re-stock date** for anything out of stock or backordered, pricing fills a throwaway "DO NOT BUY" cart, reads the priced checkout, and deletes it. Tracking searches the order book by PO number, opens every adidas order for it, and returns each delivery's carrier + tracking number (or, for an order that has not shipped, its expected ship dates). Use whenever the user needs to place/draft a PO on adidas Click, check live stock levels, restock/back-in-stock dates, or wholesale pricing, or find tracking numbers / ship dates for adidas POs.
openclaw skills install @zmtucker/drivethru-adidas-clickadidas 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.
Host system libraries. Chromium also needs OS-level shared libraries (
libglib-2.0.so.0,libnss3, …). On a missing-libraries launch failure the tool best-effort runsplaywright install-deps(needs root), then retries. If the host still lacks them it returns aconfig_errorlisting the required apt packages. Installing system libraries needs root, so on a locked-down agent host this must be handled by the environment — the OpenClaw environment's setup script or base image should install Chromium's deps (python -m playwright install --with-deps chromium, or the apt packages the error lists). The skill can fetch the browser binary at runtime, but it cannot install OS packages without root.
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.Order tracking (
get-order-tracking) is a separate, read-only surface (the order book / order detail / delivery tracking pages) built from captured HTML: order-book search by PO → order detail → Delivery Tracking table, or the article rows' expected ship dates when nothing has shipped. It has been exercised end to end against a local replica of those captured pages, but not yet against the live portal — watch the first real run. See the "Delivery tracking" section ofreferences/order_flow_notes.md.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.
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).
Reach for get-order-tracking when the request is about shipments for one
or more adidas PO numbers — "where's PO P13434?", "get me the tracking
numbers for these POs", "has P13433 shipped yet?". It searches the order
book per PO, opens each adidas order behind it, and returns the carrier +
tracking number for every delivery — or, when an order has not shipped, its
expected ship dates. It is read-only: no cart, no order, no writes.
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.
| 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?, on_missing_product?, 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?, on_missing_product?} |
get-order-tracking | low — read-only (browser); nothing is created or modified | {po_numbers: ["P13434", "P13433"]} |
Run python3 scripts/adidas.py with no action to print the action list.
create-purchase-order inputThe 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,
"on_insufficient_stock": "pause",
"on_missing_product": "pause",
"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.
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 with confirm: true). The
result comes back status: "needs_confirmation" with an out_of_stock list
({style, size, requested, available}) and a message. 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.A backorderable size is one adidas will replenish, and the portal says when:
the size tile carries a small calendar icon whose tooltip reads "Re-stock in
Nov 8, 2026". The driver reads that date for every short line and returns it as
restock_date (as shown) plus restock_date_iso (2026-11-08) — on each
out_of_stock entry, on the order's line results, and on every check line.
Answer inventory and purchase requests with it. "3XL is out of stock" is not
a usable answer on its own; "3XL is out of stock, back Nov 8, 2026" is. So
whenever a size comes back short or out of stock, state its restock date in the
reply — and when the field is null, say "adidas posted no restock date"
rather than leaving the reader to assume one exists. The per-line note already
phrases both cases.
Two sizes never carry a date, and the difference matters:
| Case | status | Restock date |
|---|---|---|
| Out of stock / short, replenishment scheduled | backorder / short | the date — report it |
| Out of stock / short, nothing scheduled | backorder / short | null — say "no restock date posted" |
| The portal's "X" cell | unavailable | null by definition — it is never coming back, so offer removal or a substitute, never a date |
Cost note. The date lives only in a hover tooltip, so the driver hovers the icon — one hover per short size. In-stock sizes are never hovered, so a fully-in-stock check costs nothing extra.
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:
on_insufficient_stock: "order".on_insufficient_stock: "skip".lines accordingly 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.
A style adidas has no product listing for — a mistyped or wrong article
number, a base style missing its color code, or an article this account simply
is not offered — is not an error by default: it comes back as an escalation
so the caller can ask the user, exactly like the out-of-stock pause. Behavior is
set by on_missing_product:
pause (default) — nothing is ordered (even with confirm: true).
The result is status: "needs_confirmation" with a missing_products list and
a message. Stop and ask the user.skip — drop the missing style's line(s) and order the rest (the dropped
lines come back with quantity: 0 and a not offered … note).error — the pre-0.7 behavior: fail the run with an api_error.Each missing_products entry is
{style, sizes, requested, reason, detail}. reason matters:
reason | Meaning | How to treat it |
|---|---|---|
not_found | The portal said so (a "no results" page, or it bounced off the product URL). | The style is genuinely not on this account. |
unresolved | The product page never rendered a size table and never said the product was missing. | Unconfirmed, not proven absent — could be a slow page or a portal change. Worth one retry before telling the user the style doesn't exist. |
Agent guidance: on a needs_confirmation result, message the user with the
missing_products details and these choices, then resume:
lines and re-run. adidas article
numbers encode the colorway (JW4306), so a bare style without the color
code will not resolve — this is the most common cause.on_missing_product: "skip".lines and re-run.If the user pre-authorized a choice ("just skip anything adidas doesn't carry"),
set the flag up front and skip the pause. Do not re-run the same unchanged
style expecting a different answer on a not_found reason.
A check (check-inventory-pricing) never aborts on a missing style: the other
lines are still read and priced, the missing ones come back as
status: "not_found" lines, and the result's status becomes
needs_confirmation (default) so the caller escalates — on_missing_product: "skip" downgrades that to a warning instead.
Timing. A missing product is detected by polling the product page and bailing on the portal's own "no results" tell, capped at 15s — it does not sit on the full 30s selector timeout per bad style. A wrong size on a style that does exist is different: that raises immediately with the list of sizes the style offers, because the product page loaded fine.
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.
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, plus the re-stock date for anything out of stock (see "Re-stock dates" above). | 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?}:
size optional (inventory only): omit it (or pass "*" / "all") to
report every size of that style. For a pricing line a specific size is
needed (a price is per size).quantity optional: defaults to 1. 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.
Never dies on an unlisted style. A style adidas has no product page for is
reported as a not_found line plus a missing_products entry; every other line
is still read and priced. See "Missing / unlisted product handling" above.
Result (status: "checked", or needs_confirmation when a style was not
found): a lines list of
{style, size, color, requested_quantity, available, available_count, status ("in_stock"|"backorder"|"unavailable"|"not_found"), in_stock, restock_date, restock_date_iso, unit_price, line_total, note}, plus order_total (summed net, pricing only),
total_quantity, missing_products, 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.
get-order-tracking)A read-only lookup that turns PO numbers into tracking numbers. It reuses the same headed-browser login as the other actions, logs in once, then walks the POs one at a time:
/adidas/reorder/my/order-book?searchText={PO}&page=0&size=20&filterByRDD=false.
One PO commonly maps to several adidas orders; every result row is read
(the grid virtualizes rows, so the list is scrolled to the end, and further
pages are fetched by bumping page until nothing new comes back)./adidas/reorder/my/order-book/{order}.#OrderTrackingButtonLink),
click it and read the delivery table: delivery note, ship date, carrier,
tracking number (+ the carrier's tracking URL). An order can have several
deliveries, and one delivery note can carry more than one parcel — every row
is returned.{"po_numbers": ["P13434", "P13433"]}
po_numbers accepts a list, or a single comma/whitespace-separated string;
po_number (one PO) also works. Duplicates are de-duplicated.
Result (status: "checked", or needs_confirmation — see below):
| Field | What it holds |
|---|---|
pos[] | One entry per requested PO: {po_number, status, orders[], order_count, shipment_count, note}. PO status is shipped / partial / not_shipped / unreadable / not_found. |
pos[].orders[] | {order_number, po_number, order_type, status, shipments[], expected_ship_dates[], expected_ship_date, note}. Order status is shipped / not_shipped / unreadable. |
pos[].orders[].shipments[] | {delivery_note, ship_date, carrier, carrier_name, tracking_number, tracking_url}. carrier is adidas's raw code (UPSN); carrier_name is resolved from the tracking link's host (UPS, FedEx, …). |
expected_ship_dates[] | [{article, dates: [...]}] — only on an order that has not shipped. expected_ship_date is the earliest of them. |
table | A ready-to-render Markdown table of every PO / order / tracking number, with unshipped rows annotated *(expected)*. |
total_orders, total_shipments, not_found_pos, warnings | Run-level summary. |
Reporting back to the user: hand back the table field (optionally
re-formatted) rather than re-deriving one — it already groups by PO and marks
which dates are expected rather than actual:
| PO | Order # | Delivery Note | Ship Date | Carrier | Tracking # | Note |
|---|---|---|---|---|---|---|
| P13434 | 6279266468 | 7342219301 | Aug 3, 2026 | UPS | 1Z2AT4600373532427 | shipped |
| P13434 | 6279266469 | — | Feb 4, 2027 (expected) | — | not shipped yet | not shipped — expected ship date (IK1234) |
When the result is needs_confirmation the lookup is incomplete —
everything reported is accurate, but something is missing:
not_found_pos — the order book returned no order carrying that PO.
Usually a wrong/mistyped PO number. adidas's search also matches PO
prefixes, so if the search surfaced orders for other POs, a warning
names them (e.g. searching P1343 surfaces P13433 / P13434) — those are
not tracked, because they are not the PO that was asked for. Take the PO
number back to the user.unreadable order — the order page never rendered. That order's status
is unknown (it is never reported as "not shipped"); a warning names it. Worth
one retry before telling the user anything about it.Security note: each delivery row also carries a PDF download link whose URL embeds a bearer access token. That link is deliberately not read or returned — do not go fetch it.
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 host
Inline in the stdin JSON — pass username, password, and base_url
alongside the tool's own arguments.
CLI flags — --username / --password / --base-url after 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.
create-purchase-order is the only order-placing action.
"confirm": true to 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.confirm: true for an order the user actually intends to
place.get-order-tracking is read-only: it only navigates and reads order-book,
order, and delivery-tracking pages. No cart is created, nothing is ordered, and
no portal state changes.
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.
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:
ADIDAS_CLICK_HEADLESS=true to force headless,
which will most likely time out at the login page);--disable-blink-features=AutomationControlled, a
navigator.webdriver mask, and a longer navigation timeout;xauth needed) and points
$DISPLAY at it — this requires the xvfb system 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.
Failures print {"error": {...}} and exit non-zero:
config_error (exit 2) — missing/invalid credentials, a bad policy value, or
an un-captured step.api_error — the portal rejected an action or a page did not match
expectations. Includes surface, operation, retryable. Note that a style
adidas does not carry is not an api_error by default — it comes back as
a needs_confirmation result (see "Missing / unlisted product handling"), and
only becomes an api_error under on_missing_product: "error".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 valid actions.Surface the human-readable message to the user. Do not retry on
config_error or validation_error.
references/order_flow_notes.md — the
reverse-engineered portal flows: the ordering step map and captured selectors,
plus the read-only delivery tracking walk (order book → order → deliveries
/ expected ship dates). Start here to continue the build.