FluxA Agentic Checkout Skill version: 0.4.0 | Product surface: deterministic checkout automation + explicit human handoff Overview Use this skill as the release-grade operator runbook for checkout automation. It accepts arbitrary entry links, attempts the currently implemented automation routes, and produces structured results that tell the next agent or human whether the checkout is ready, partially filled, blocked by verification, or requires manual takeover. How to do checkout for users Step 1: Check whether the URL supports automated checkout 1. Check whether the user has provided a speci…

), button[aria-label*='

FluxA Agentic Checkout Skill version: 0.4.0 | Product surface: deterministic checkout automation + explicit human handoff Overview Use this skill as the release-grade operator runbook for checkout automation. It accepts arbitrary entry links, attempts the currently implemented automation routes, and produces structured results that tell the next agent or human whether the checkout is ready, partially filled, blocked by verification, or requires manual takeover. How to do checkout for users Step 1: Check whether the URL supports automated checkout 1. Check whether the user has provided a speci…

], [role='button']:has-text('

FluxA Agentic Checkout Skill version: 0.4.0 | Product surface: deterministic checkout automation + explicit human handoff Overview Use this skill as the release-grade operator runbook for checkout automation. It accepts arbitrary entry links, attempts the currently implemented automation routes, and produces structured results that tell the next agent or human whether the checkout is ready, partially filled, blocked by verification, or requires manual takeover. How to do checkout for users Step 1: Check whether the URL supports automated checkout 1. Check whether the user has provided a speci…

)\"\n )\n try:\n button_count = min(await amount_buttons.count(), 30)\n except Exception:\n button_count = 0\n button_choices = []\n for index in range(button_count):\n button = amount_buttons.nth(index)\n try:\n text = normalize_text(await button.inner_text())\n aria = normalize_text(await button.get_attribute(\"aria-label\"))\n disabled = await button.is_disabled()\n except Exception:\n continue\n haystack = \" \".join(part for part in (text, aria) if part)\n amount = parse_money_amount(haystack)\n if amount is None or disabled:\n continue\n if \"select\" not in haystack.lower() and \"amount\" not in haystack.lower() and \"gift\" not in haystack.lower():\n continue\n button_choices.append((amount, index, text or aria))\n if button_choices:\n button_choices.sort(key=lambda item: item[0])\n _amount, index, text = button_choices[0]\n button = amount_buttons.nth(index)\n try:\n await button.click(timeout=3_000)\n except Exception:\n try:\n await button.click(force=True, timeout=3_000)\n except Exception:\n pass\n await pause(config, 0.6)\n return text\n\n radios = page.locator(\"input[type='radio']\")\n choices: list[tuple[float, int, str]] = []\n try:\n count = min(await radios.count(), 20)\n except Exception:\n count = 0\n\n radio_js = \"\"\"(el) => {\n const parts = [];\n const push = (value) => {\n if (value) parts.push(String(value).replace(/\\\\s+/g, ' ').trim());\n };\n push(el.value);\n if (el.id) {\n const label = document.querySelector(`label[for=\"${CSS.escape(el.id)}\"]`);\n if (label) push(label.innerText);\n }\n const parentLabel = el.closest('label');\n if (parentLabel) push(parentLabel.innerText);\n if (el.parentElement) push(el.parentElement.innerText);\n return parts.join(' ').trim();\n }\"\"\"\n for index in range(count):\n radio = radios.nth(index)\n try:\n text = normalize_text(await radio.evaluate(radio_js))\n except Exception:\n continue\n amount = parse_money_amount(text)\n if amount is None:\n continue\n choices.append((amount, index, normalize_selection_label(text) or text))\n\n if choices:\n choices.sort(key=lambda item: item[0])\n _amount, index, text = choices[0]\n radio = radios.nth(index)\n clicked = False\n try:\n radio_id = await radio.get_attribute(\"id\")\n except Exception:\n radio_id = None\n if radio_id:\n try:\n label = page.locator(f\"label[for='{radio_id}']\").first\n if await label.count() > 0:\n await label.click(timeout=3_000)\n clicked = True\n except Exception:\n pass\n if not clicked:\n try:\n await radio.check(force=True, timeout=3_000)\n clicked = True\n except Exception:\n pass\n if not clicked:\n try:\n await radio.click(force=True, timeout=3_000)\n except Exception:\n pass\n await pause(config, 0.4)\n return text\n\n selects = page.locator(\"select\")\n try:\n select_count = min(await selects.count(), 4)\n except Exception:\n select_count = 0\n for index in range(select_count):\n select = selects.nth(index)\n option_locator = select.locator(\"option\")\n try:\n option_count = min(await option_locator.count(), 40)\n except Exception:\n continue\n choices = []\n for option_index in range(option_count):\n option = option_locator.nth(option_index)\n try:\n text = normalize_text(await option.inner_text())\n value = normalize_text(await option.get_attribute(\"value\"))\n except Exception:\n continue\n amount = parse_money_amount(text)\n if amount is None or not value:\n continue\n choices.append((amount, value, normalize_selection_label(text) or text))\n if not choices:\n continue\n choices.sort(key=lambda item: item[0])\n _amount, value, text = choices[0]\n try:\n await select.select_option(value=value)\n await pause(config, 0.4)\n return text\n except Exception:\n continue\n return None\n\n\nasync def _add_to_cart(page: Any, config: Any) -> bool:\n await _dismiss_banners(page, config)\n if await click_first_visible(page, ADD_TO_CART_SELECTORS, timeout_ms=5_000):\n await pause(config, 1.0)\n await _dismiss_banners(page, config)\n if await _wait_for_cart_ready(page):\n return True\n try:\n form = page.locator(\"form[action*='/cart/add']\").first\n if await form.count() > 0:\n await form.evaluate(\"(node) => node.requestSubmit ? node.requestSubmit() : node.submit()\")\n await pause(config, 1.0)\n await _dismiss_banners(page, config)\n if await _wait_for_cart_ready(page):\n return True\n except Exception:\n pass\n resolved_variant_id = await _resolve_selected_variant_id(page)\n if resolved_variant_id and await _post_cart_add_variant(page, resolved_variant_id):\n await pause(config, 1.0)\n await _dismiss_banners(page, config)\n if await _wait_for_cart_ready(page):\n return True\n try:\n added = await page.evaluate(\n \"\"\"async () => {\n const variantFromUrl = new URL(window.location.href).searchParams.get('variant');\n const post = async (body, headers = {}) => {\n const response = await fetch('/cart/add.js', {\n method: 'POST',\n body,\n credentials: 'same-origin',\n headers: {accept: 'application/json', ...headers},\n });\n return response.ok;\n };\n\n const form = document.querySelector(\"form[action*='/cart/add']\");\n if (form) {\n const formData = new FormData(form);\n if (!formData.get('id') && variantFromUrl) formData.set('id', variantFromUrl);\n if (!formData.get('quantity')) formData.set('quantity', '1');\n if (formData.get('id')) {\n try {\n if (await post(formData)) return true;\n } catch {}\n }\n }\n\n if (variantFromUrl) {\n const params = new URLSearchParams();\n params.set('id', variantFromUrl);\n params.set('quantity', '1');\n try {\n if (\n await post(params, {\n 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',\n })\n ) return true;\n } catch {}\n }\n return false;\n }\"\"\"\n )\n if added:\n await pause(config, 1.0)\n await _dismiss_banners(page, config)\n if await _wait_for_cart_ready(page):\n return True\n except Exception:\n pass\n return False\n\n\nasync def _go_to_checkout(page: Any, config: Any) -> bool:\n await _dismiss_banners(page, config)\n if await _is_checkout_ready(page):\n return True\n\n if await click_first_visible(page, CHECKOUT_SELECTORS, timeout_ms=5_000):\n await pause(config, 0.8)\n await _dismiss_banners(page, config)\n if await _wait_for_checkout(page):\n return True\n\n checkout_url = await _extract_checkout_url(page)\n if checkout_url:\n await _goto_best_effort(page, checkout_url, timeout_ms=45_000)\n await pause(config, 0.8)\n await _dismiss_banners(page, config)\n if await _wait_for_checkout(page):\n return True\n\n parsed = urlparse(page.url)\n origin = f\"{parsed.scheme}://{parsed.netloc}\" if parsed.scheme and parsed.netloc else \"\"\n if not origin:\n return False\n\n cart_url = f\"{origin}/cart\"\n await _goto_best_effort(page, cart_url, timeout_ms=45_000)\n await pause(config, 0.5)\n await _dismiss_banners(page, config)\n if await click_first_visible(page, CHECKOUT_SELECTORS, timeout_ms=5_000):\n await pause(config, 0.8)\n await _dismiss_banners(page, config)\n if await _wait_for_checkout(page):\n return True\n\n checkout_url = await _extract_checkout_url(page) or f\"{origin}/checkout?skip_shop_pay=true\"\n await _goto_best_effort(page, checkout_url, timeout_ms=45_000)\n await pause(config, 0.8)\n await _dismiss_banners(page, config)\n return await _wait_for_checkout(page, timeout_seconds=12.0)\n\n\nasync def run_navigation_agent(\n config: Any,\n run_id: str,\n user_data_dir: Path | None,\n traces_dir: Path,\n videos_dir: Path | None,\n conversations_dir: Path,\n ranked_candidates: list[dict[str, Any]] | None = None,\n) -> dict[str, Any]:\n if Agent is None or Browser is None or BrowserProfile is None:\n raise RuntimeError(f\"browser-use is required for Shopify navigation: {BROWSER_USE_IMPORT_ERROR}\")\n\n ranked_candidates = ranked_candidates or rank_candidates(config.candidate_urls_json, query=config.query, limit=3)\n if not ranked_candidates:\n payload = default_payload()\n payload[\"outcome\"] = {\"status\": \"no_shopify_candidate\", \"hint\": \"No viable Shopify checkout candidate was available.\"}\n payload[\"hint\"] = \"No viable Shopify checkout candidate was available.\"\n return {\"navigation\": payload, \"artifacts\": {\"conversation\": None, \"screenshots\": [], \"urls\": [], \"method\": \"browser_use\"}}\n\n allowed_domains = build_allowed_domains(ranked_candidates)\n llm = resolve_llm(config)\n conversation_path = conversations_dir / f\"shopify-navigation-{run_id}.json\"\n\n browser_profile = BrowserProfile(\n headless=not config.headed,\n channel=config.browser_channel,\n user_data_dir=user_data_dir,\n record_video_dir=videos_dir if config.record_video else None,\n traces_dir=traces_dir if config.record_trace else None,\n enable_default_extensions=False,\n proxy=(\n ProxySettings(\n server=config.proxy_server,\n bypass=config.proxy_bypass,\n username=config.proxy_username,\n password=config.proxy_password,\n )\n if config.proxy_server\n else None\n ),\n allowed_domains=allowed_domains,\n wait_between_actions=max(config.action_delay_seconds, 0.25),\n minimum_wait_page_load_time=0.1,\n wait_for_network_idle_page_load_time=0.1,\n highlight_elements=False,\n cross_origin_iframes=True,\n captcha_solver=False,\n keep_alive=True,\n )\n\n browser = Browser(browser_profile=browser_profile)\n agent = Agent(\n task=build_navigation_task(config, ranked_candidates),\n llm=llm,\n browser=browser,\n sensitive_data={},\n save_conversation_path=conversation_path,\n use_vision=config.use_vision,\n max_failures=4,\n max_actions_per_step=5,\n step_timeout=min(max(config.max_run_seconds, 90), 600),\n source=\"agent-shopify-checkout-browser-use\",\n )\n history = None\n handoff_runtime: dict[str, Any] | None = None\n try:\n history = await asyncio.wait_for(agent.run(max_steps=config.max_steps), timeout=config.max_run_seconds)\n screenshots = []\n try:\n screenshots = [path for path in history.screenshot_paths(return_none_if_not_screenshot=False) if path]\n except TypeError:\n screenshots = [path for path in history.screenshot_paths() if path]\n parsed = extract_json_object(history.final_result())\n normalized = normalize_navigation_result(parsed, [item[\"url\"] for item in ranked_candidates], history=history)\n if normalized.get(\"outcome\", {}).get(\"status\") == \"checkout_reached\" and getattr(browser, \"cdp_url\", None):\n handoff_runtime = {\n \"handoff\": \"browser_use_cdp\",\n \"cdp_url\": browser.cdp_url,\n \"browser_session\": browser,\n \"checkout_url\": normalized.get(\"checkoutUrl\"),\n }\n result = {\n \"navigation\": normalized,\n \"artifacts\": {\n \"conversation\": str(conversation_path),\n \"screenshots\": screenshots,\n \"urls\": history.urls(),\n \"errors\": history.errors(),\n \"actions\": history.action_names(),\n \"rankedCandidates\": ranked_candidates,\n \"method\": \"browser_use\",\n },\n }\n if handoff_runtime is not None:\n result[\"runtime\"] = handoff_runtime\n return result\n finally:\n if handoff_runtime is None:\n stop = getattr(browser, \"stop\", None)\n if callable(stop):\n await stop()\n\n\nasync def run_navigation_playwright_fallback(\n config: Any,\n run_id: str,\n user_data_dir: Path | None,\n traces_dir: Path,\n videos_dir: Path | None,\n conversations_dir: Path,\n ranked_candidates: list[dict[str, Any]],\n fallback_reason: str | None = None,\n) -> dict[str, Any]:\n if async_playwright is None:\n raise RuntimeError(f\"Playwright is required for Shopify navigation fallback: {PLAYWRIGHT_IMPORT_ERROR}\")\n\n artifacts: dict[str, Any] = {\n \"conversation\": None,\n \"screenshots\": [],\n \"urls\": [],\n \"errors\": [],\n \"rankedCandidates\": ranked_candidates,\n \"method\": \"playwright_fallback\",\n }\n if fallback_reason:\n artifacts[\"errors\"].append(f\"browser_use_fallback:{fallback_reason}\")\n\n screenshot_dir = conversations_dir.parent / \"screenshots\" / run_id\n best_payload: dict[str, Any] | None = None\n playwright = None\n context = None\n page = None\n trace_path = traces_dir / f\"navigation-fallback-{run_id}.zip\"\n handoff_runtime: dict[str, Any] | None = None\n try:\n playwright = await async_playwright().start() # pragma: no cover - runtime integration\n context = await playwright.chromium.launch_persistent_context(\n user_data_dir=str(user_data_dir or conversations_dir.parent / \"shopify-navigation-profile\"),\n headless=not config.headed,\n channel=config.browser_channel,\n ignore_https_errors=True,\n record_video_dir=str(videos_dir) if videos_dir else None,\n proxy=_proxy_settings(config),\n )\n page = context.pages[0] if context.pages else await context.new_page()\n if config.record_trace:\n await context.tracing.start(screenshots=True, snapshots=True, sources=False)\n\n for index, candidate in enumerate(ranked_candidates, start=1):\n candidate_url = candidate[\"url\"]\n try:\n current_url = normalize_text(page.url if page else \"\")\n same_store = urlparse(current_url).netloc.lower() == urlparse(candidate_url).netloc.lower()\n if not current_url or current_url == \"about:blank\" or (not same_store and not is_checkout_url(current_url)):\n await _goto_best_effort(page, candidate_url, timeout_ms=45_000)\n artifacts[\"urls\"].append(page.url)\n await _dismiss_banners(page, config)\n\n if await _wait_for_checkout(page, timeout_seconds=3.0):\n checkout_shot = await _save_screenshot(page, screenshot_dir, f\"{index:02d}-checkout\")\n if checkout_shot:\n artifacts[\"screenshots\"].append(checkout_shot)\n payload = _build_payload(\n \"checkout_reached\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=candidate_url if \"/products/\" in candidate_url else None,\n checkout_url=page.url,\n hint=\"Entry URL redirected straight to Shopify checkout.\",\n )\n handoff_runtime = {\n \"playwright\": playwright,\n \"context\": context,\n \"page\": page,\n \"trace_path\": str(trace_path),\n \"trace_started\": bool(config.record_trace),\n }\n return {\"navigation\": payload, \"artifacts\": artifacts, \"runtime\": handoff_runtime}\n\n body = await _body_text(page)\n if looks_like_security_verification(body):\n payload = _build_payload(\n \"needs_manual_verification\",\n candidate_url=candidate_url,\n current_url=page.url,\n hint=\"Store requires CAPTCHA, Cloudflare, or another manual verification step before product selection.\",\n )\n best_payload = _better_payload(best_payload, payload)\n return {\"navigation\": payload, \"artifacts\": artifacts}\n\n product_url = await _resolve_product_page(page, config)\n if not product_url:\n payload = _build_payload(\n \"candidate_selected\",\n candidate_url=candidate_url,\n current_url=page.url,\n hint=\"Candidate opened, but no visible Shopify product link was confirmed.\",\n )\n best_payload = _better_payload(best_payload, payload)\n continue\n\n product_shot = await _save_screenshot(page, screenshot_dir, f\"{index:02d}-product\")\n if product_shot:\n artifacts[\"screenshots\"].append(product_shot)\n\n denomination = await _select_lowest_denomination(page, config)\n added_to_cart = await _add_to_cart(page, config)\n if not added_to_cart:\n if await _go_to_checkout_via_cart_permalink(page, config):\n artifacts[\"urls\"].append(page.url)\n checkout_shot = await _save_screenshot(page, screenshot_dir, f\"{index:02d}-checkout\")\n if checkout_shot:\n artifacts[\"screenshots\"].append(checkout_shot)\n\n body = await _body_text(page)\n if looks_like_security_verification(body):\n payload = _build_payload(\n \"needs_manual_verification\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=product_url,\n checkout_url=page.url,\n denomination=denomination,\n hint=\"Checkout was reached via cart permalink fallback, but manual verification is required.\",\n )\n best_payload = _better_payload(best_payload, payload)\n return {\"navigation\": payload, \"artifacts\": artifacts}\n\n payload = _build_payload(\n \"checkout_reached\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=product_url,\n checkout_url=page.url,\n denomination=denomination,\n hint=\"Reached Shopify checkout via cart permalink fallback after direct add-to-cart failed.\",\n )\n handoff_runtime = {\n \"playwright\": playwright,\n \"context\": context,\n \"page\": page,\n \"trace_path\": str(trace_path),\n \"trace_started\": bool(config.record_trace),\n }\n return {\"navigation\": payload, \"artifacts\": artifacts, \"runtime\": handoff_runtime}\n payload = _build_payload(\n \"product_selected\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=product_url,\n denomination=denomination,\n hint=\"Shopify product was confirmed, but Add to cart could not be completed.\",\n )\n best_payload = _better_payload(best_payload, payload)\n continue\n\n if not await _go_to_checkout(page, config):\n payload = _build_payload(\n \"product_selected\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=product_url,\n denomination=denomination,\n hint=\"Shopify product was added to cart, but checkout was not reached.\",\n )\n best_payload = _better_payload(best_payload, payload)\n continue\n\n artifacts[\"urls\"].append(page.url)\n checkout_shot = await _save_screenshot(page, screenshot_dir, f\"{index:02d}-checkout\")\n if checkout_shot:\n artifacts[\"screenshots\"].append(checkout_shot)\n\n body = await _body_text(page)\n if looks_like_security_verification(body):\n payload = _build_payload(\n \"needs_manual_verification\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=product_url,\n checkout_url=page.url,\n denomination=denomination,\n hint=\"Checkout requires CAPTCHA, Cloudflare, or another manual verification step.\",\n )\n best_payload = _better_payload(best_payload, payload)\n return {\"navigation\": payload, \"artifacts\": artifacts}\n\n payload = _build_payload(\n \"checkout_reached\",\n candidate_url=candidate_url,\n current_url=page.url,\n product_url=product_url,\n checkout_url=page.url,\n denomination=denomination,\n hint=\"Reached Shopify checkout via deterministic Playwright fallback.\",\n )\n handoff_runtime = {\n \"playwright\": playwright,\n \"context\": context,\n \"page\": page,\n \"trace_path\": str(trace_path),\n \"trace_started\": bool(config.record_trace),\n }\n return {\"navigation\": payload, \"artifacts\": artifacts, \"runtime\": handoff_runtime}\n except Exception as exc:\n artifacts[\"errors\"].append(f\"{candidate_url}: {type(exc).__name__}: {exc}\")\n payload = recover_navigation_from_current_url(\n page.url if page else None,\n ranked_candidates,\n error_text=f\"{type(exc).__name__}: {exc}\",\n ) or _build_payload(\n \"candidate_selected\",\n candidate_url=candidate_url,\n current_url=page.url if page else None,\n hint=f\"Candidate opened but navigation failed: {type(exc).__name__}: {exc}\",\n )\n best_payload = _better_payload(best_payload, payload)\n\n if best_payload is None:\n best_payload = _build_payload(\n \"no_shopify_candidate\",\n candidate_url=ranked_candidates[0][\"url\"] if ranked_candidates else None,\n current_url=None,\n hint=\"No viable Shopify checkout candidate was available.\",\n )\n return {\"navigation\": best_payload, \"artifacts\": artifacts}\n finally:\n if handoff_runtime is None:\n if context is not None:\n if config.record_trace:\n try:\n await context.tracing.stop(path=str(trace_path))\n except Exception:\n pass\n try:\n await asyncio.wait_for(context.close(), timeout=10)\n except Exception:\n pass\n if playwright is not None:\n try:\n await asyncio.wait_for(playwright.stop(), timeout=5)\n except Exception:\n pass\n\n\nasync def run_navigation(\n config: Any,\n run_id: str,\n user_data_dir: Path | None,\n traces_dir: Path,\n videos_dir: Path | None,\n conversations_dir: Path,\n) -> dict[str, Any]:\n ranked_candidates = rank_candidates(config.candidate_urls_json, query=config.query, limit=3)\n if not ranked_candidates:\n payload = _build_payload(\n \"no_shopify_candidate\",\n candidate_url=None,\n current_url=None,\n hint=\"No viable Shopify checkout candidate was available.\",\n )\n return {\n \"navigation\": payload,\n \"artifacts\": {\"conversation\": None, \"screenshots\": [], \"urls\": [], \"errors\": [], \"rankedCandidates\": [], \"method\": \"none\"},\n }\n\n browser_use_error: str | None = None\n try:\n result = await run_navigation_agent(\n config=config,\n run_id=run_id,\n user_data_dir=user_data_dir,\n traces_dir=traces_dir,\n videos_dir=videos_dir,\n conversations_dir=conversations_dir,\n ranked_candidates=ranked_candidates,\n )\n status = result[\"navigation\"].get(\"outcome\", {}).get(\"status\", \"unknown\")\n if status in {\"checkout_reached\", \"needs_manual_verification\", \"no_shopify_candidate\"}:\n return result\n if status not in {\"unknown\", \"candidate_selected\", \"product_selected\"}:\n return result\n browser_use_error = f\"partial_navigation:{status}\"\n except Exception as exc:\n browser_use_error = f\"{type(exc).__name__}: {exc}\"\n\n return await run_navigation_playwright_fallback(\n config=config,\n run_id=run_id,\n user_data_dir=user_data_dir,\n traces_dir=traces_dir,\n videos_dir=videos_dir,\n conversations_dir=conversations_dir,\n ranked_candidates=ranked_candidates,\n fallback_reason=browser_use_error,\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":55423,"content_sha256":"944937e991c25ac69b6d93f030251f92cb8938a9c7134291b6f4393f277bcf00"},{"filename":"scripts/shopify/providers.py","content":"#!/usr/bin/env python3\n\"\"\"Provider detection helpers for Shopify checkout pages.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\nimport re\n\n\nUNSUPPORTED_MARKERS = (\"paypal\", \"braintree\", \"adyen\", \"klarna\", \"afterpay\", \"amazon pay\")\nCARD_FIELD_MARKERS = (\n \"card number\",\n \"name on card\",\n \"expiration date\",\n \"security code\",\n \"mm / yy\",\n \"cvc\",\n \"cvv\",\n \"autocomplete=\\\"cc-number\\\"\",\n \"autocomplete='cc-number'\",\n)\n\n\ndef normalize_text(value: Any) -> str:\n return re.sub(r\"\\s+\", \" \", str(value or \"\")).strip()\n\n\ndef detect_provider(\n current_url: str,\n html: str,\n iframe_urls: list[str] | None = None,\n button_texts: list[str] | None = None,\n) -> dict[str, Any]:\n iframe_urls = iframe_urls or []\n button_texts = button_texts or []\n haystack = \" \".join(\n [\n normalize_text(current_url).lower(),\n normalize_text(html).lower(),\n \" \".join(normalize_text(item).lower() for item in iframe_urls),\n \" \".join(normalize_text(item).lower() for item in button_texts),\n ]\n )\n hints: list[str] = []\n has_card_fields = any(marker in haystack for marker in CARD_FIELD_MARKERS)\n\n if any(\"js.stripe.com\" in iframe.lower() or \"stripe\" in iframe.lower() for iframe in iframe_urls):\n hints.append(\"iframe:stripe\")\n return {\"provider\": \"stripe_hosted\", \"hints\": hints}\n\n if any(host in normalize_text(current_url).lower() for host in (\"checkout.stripe.com\", \"buy.stripe.com\", \"billing.stripe.com\")):\n hints.append(\"url:stripe_checkout\")\n return {\"provider\": \"stripe_hosted\", \"hints\": hints}\n\n if \"secure payment input frame\" in haystack or \"autocomplete=\\\"cc-number\\\"\" in haystack:\n hints.append(\"dom:stripe_card_markers\")\n return {\"provider\": \"stripe_hosted\", \"hints\": hints}\n\n if any(\"checkout.pci.shopifyinc.com\" in iframe.lower() for iframe in iframe_urls):\n hints.append(\"iframe:shopify_pci\")\n return {\"provider\": \"shopify_checkout_card\", \"hints\": hints}\n\n if (\n \"/checkouts/\" in normalize_text(current_url).lower()\n and \"credit card\" in haystack\n and (\"field container for: card number\" in haystack or \"expiration date (mm / yy)\" in haystack)\n ):\n hints.append(\"dom:shopify_checkout_card\")\n return {\"provider\": \"shopify_checkout_card\", \"hints\": hints}\n\n if \"pay.shopify.com\" in normalize_text(current_url).lower() and has_card_fields:\n hints.append(\"url:shop_pay\")\n return {\"provider\": \"shop_pay_card\", \"hints\": hints}\n\n if any(\"pay.shopify.com\" in iframe.lower() or \"shop.app\" in iframe.lower() for iframe in iframe_urls) and has_card_fields:\n hints.append(\"iframe:shop_pay\")\n return {\"provider\": \"shop_pay_card\", \"hints\": hints}\n\n if \"shop pay\" in haystack and has_card_fields:\n hints.append(\"dom:shop_pay\")\n return {\"provider\": \"shop_pay_card\", \"hints\": hints}\n\n unsupported = [marker for marker in UNSUPPORTED_MARKERS if marker in haystack]\n if unsupported:\n for marker in unsupported:\n hints.append(f\"unsupported:{marker}\")\n return {\"provider\": \"unsupported\", \"hints\": hints}\n\n hints.append(\"provider:unknown\")\n return {\"provider\": \"unknown\", \"hints\": hints}\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3280,"content_sha256":"49e26f4ed756c0f56ac71bdc8709b12ed4df1f7f8121321dc28c2e5b72d7cb3f"},{"filename":"scripts/shopify/results.py","content":"#!/usr/bin/env python3\n\"\"\"Structured result helpers for the checkout handoff flow.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\n\nFINAL_PHASES = {\n \"no_shopify_candidate\",\n \"candidate_selected\",\n \"product_selected\",\n \"checkout_reached\",\n \"payment_details_filled\",\n \"preview_complete\",\n \"execute_success\",\n \"execute_fail\",\n \"unsupported_provider\",\n \"needs_manual_verification\",\n \"limit_exceeded\",\n \"timeout\",\n \"config_error\",\n \"exception\",\n}\n\n\ndef default_payload() -> dict[str, Any]:\n return {\n \"entryUrl\": None,\n \"candidateChosen\": None,\n \"storeDomain\": None,\n \"productUrl\": None,\n \"checkoutUrl\": None,\n \"selectionLabel\": None,\n \"provider\": None,\n \"displayedTotal\": None,\n \"giftCardDenomination\": None,\n \"contactFilled\": False,\n \"deliveryFilled\": None,\n \"billingIdentityFilled\": None,\n \"postalFilled\": False,\n \"residentIdFilled\": None,\n \"blockingFields\": [],\n \"legalConsentChecked\": None,\n \"saveInfoUnchecked\": None,\n \"handoffRequired\": False,\n \"handoffReason\": None,\n \"filledCheckoutScreenshot\": None,\n \"userMessage\": None,\n \"hint\": \"\",\n \"outcome\": {\"status\": \"unknown\", \"hint\": None},\n }\n\n\ndef _build_user_message(data: dict[str, Any], phase: str) -> str:\n link = data.get(\"checkoutUrl\") or data.get(\"productUrl\") or data.get(\"entryUrl\") or data.get(\"candidateChosen\")\n blocking_fields = [str(item).strip() for item in (data.get(\"blockingFields\") or []) if str(item).strip()]\n def with_link(base: str) -> str:\n if link:\n return f\"{base} You can continue from this link: {link}\"\n return base\n if phase == \"preview_complete\":\n return \"I have filled in the checkout information up to the point right before payment. Please confirm that the Delivery and Billing information is correct before deciding whether to click Pay now.\"\n if phase == \"payment_details_filled\":\n if blocking_fields:\n labels = \", \".join(blocking_fields[:3])\n base = f\"I have already entered checkout and filled most of the information. However, this merchant still requires {labels}, so this step needs to be completed manually.\"\n else:\n base = \"I have already entered checkout, but some Delivery, Billing, or payment details could not be filled reliably, so you need to continue manually from here.\"\n return with_link(base)\n if phase in {\"unsupported_provider\", \"needs_manual_verification\"}:\n base = \"I have already progressed the flow to checkout, but the current page requires manual action and cannot be completed fully automatically right now.\"\n return with_link(base)\n if phase == \"limit_exceeded\":\n return \"I stopped here because the current item total exceeds the allowed limit for this run.\"\n if phase == \"execute_fail\":\n base = \"I could not complete the final payment submission yet. Please check the Delivery, Billing, and card information before continuing the checkout.\"\n return with_link(base)\n if phase == \"execute_success\":\n return \"The checkout submission completed successfully.\"\n if phase == \"checkout_reached\":\n return with_link(\"I have opened the checkout page, but the information has not yet been filled completely to a payable state.\")\n if phase == \"product_selected\":\n return with_link(\"I have opened the product page, but I could not add the item to the cart reliably yet. You can try again later, or send me this link so I can continue investigating.\")\n if phase == \"candidate_selected\":\n return with_link(\"I have opened the merchant page, but I could not yet identify a product or checkout entry that can be advanced directly. Send me a more specific product link and I can continue from there.\")\n if phase == \"no_shopify_candidate\":\n return \"I could not find a product page that can continue through automated checkout yet. Give me a more specific product link and I can keep moving this forward.\"\n return \"\"\n\n\ndef normalize_checkout_state(\n mode: str,\n payload: dict[str, Any] | None = None,\n max_total_usd: float | None = None,\n) -> dict[str, Any]:\n data = default_payload()\n if payload:\n data.update(payload)\n if payload.get(\"outcome\"):\n data[\"outcome\"] = {\"status\": \"unknown\", \"hint\": None, **payload[\"outcome\"]}\n if not data.get(\"selectionLabel\") and data.get(\"giftCardDenomination\"):\n data[\"selectionLabel\"] = data[\"giftCardDenomination\"]\n if not data.get(\"entryUrl\"):\n data[\"entryUrl\"] = data.get(\"checkoutUrl\") or data.get(\"productUrl\") or data.get(\"candidateChosen\")\n\n displayed_total = data.get(\"displayedTotal\")\n provider = data.get(\"provider\") or \"unknown\"\n contact_filled = bool(data.get(\"contactFilled\"))\n delivery_filled = data.get(\"deliveryFilled\")\n billing_identity_filled = data.get(\"billingIdentityFilled\")\n postal_filled = bool(data.get(\"postalFilled\"))\n blocking_fields = [str(item).strip() for item in (data.get(\"blockingFields\") or []) if str(item).strip()]\n legal_consent_checked = data.get(\"legalConsentChecked\")\n if legal_consent_checked is False and \"Sensitive personal information consent\" not in blocking_fields:\n blocking_fields.append(\"Sensitive personal information consent\")\n data[\"blockingFields\"] = blocking_fields\n save_info_unchecked = data.get(\"saveInfoUnchecked\")\n outcome_status = str(data.get(\"outcome\", {}).get(\"status\") or \"unknown\")\n hint = str(data.get(\"hint\") or data.get(\"outcome\", {}).get(\"hint\") or \"\").strip()\n\n if not data.get(\"checkoutUrl\"):\n if data.get(\"productUrl\"):\n phase = \"product_selected\"\n elif data.get(\"candidateChosen\"):\n phase = \"candidate_selected\"\n else:\n phase = \"no_shopify_candidate\"\n if outcome_status == \"unknown\":\n outcome_status = phase\n elif max_total_usd is not None and displayed_total is not None and displayed_total > max_total_usd:\n phase = \"limit_exceeded\"\n outcome_status = \"limit_exceeded\"\n elif outcome_status in {\"needs_manual_verification\", \"needs_3ds\"}:\n phase = \"needs_manual_verification\"\n elif blocking_fields:\n phase = \"execute_fail\" if mode == \"execute\" else \"payment_details_filled\"\n outcome_status = phase\n elif provider in {\"unsupported\", \"unknown\"}:\n phase = \"unsupported_provider\"\n if outcome_status == \"unknown\":\n outcome_status = \"unsupported_provider\" if provider == \"unsupported\" else \"unknown\"\n elif mode == \"execute\":\n if outcome_status == \"execute_success\":\n phase = \"execute_success\"\n elif (\n not contact_filled\n or delivery_filled is False\n or billing_identity_filled is False\n or not postal_filled\n or legal_consent_checked is False\n or save_info_unchecked is False\n ):\n phase = \"execute_fail\"\n if outcome_status == \"unknown\":\n outcome_status = \"execute_fail\"\n else:\n phase = \"execute_fail\" if outcome_status == \"execute_fail\" else \"payment_details_filled\"\n else:\n if (\n contact_filled\n and delivery_filled is not False\n and billing_identity_filled is not False\n and postal_filled\n and legal_consent_checked is not False\n and save_info_unchecked is not False\n ):\n phase = \"preview_complete\"\n outcome_status = \"preview_complete\"\n elif contact_filled or delivery_filled or billing_identity_filled or postal_filled:\n phase = \"payment_details_filled\"\n else:\n phase = \"checkout_reached\"\n\n if not hint:\n hint = {\n \"preview_complete\": \"Checkout is filled and ready for manual confirmation.\",\n \"payment_details_filled\": \"Some payment details were filled, but required validation is incomplete.\",\n \"checkout_reached\": \"Reached checkout but payment details were not filled yet.\",\n \"product_selected\": \"Reached a product page but not checkout yet.\",\n \"candidate_selected\": \"Selected an entry page but did not confirm a supported checkout path yet.\",\n \"no_shopify_candidate\": \"No supported entry page was selected.\",\n \"unsupported_provider\": \"Detected a checkout surface outside the currently implemented adapters. Hand off to a human operator.\",\n \"limit_exceeded\": \"Checkout total exceeded the configured safety limit.\",\n \"needs_manual_verification\": \"Manual verification or issuer authentication is required. Hand off to a human operator.\",\n \"execute_success\": \"Payment submission reached a success state.\",\n \"execute_fail\": \"Payment submission failed or could not be safely completed.\",\n }.get(phase, \"Checkout run completed.\")\n\n handoff_required = False\n handoff_reason = None\n if phase == \"unsupported_provider\":\n handoff_required = True\n handoff_reason = \"unsupported_provider\"\n elif phase == \"needs_manual_verification\":\n handoff_required = True\n handoff_reason = \"3ds_authentication\" if outcome_status == \"needs_3ds\" else \"manual_verification\"\n elif phase in {\"payment_details_filled\", \"checkout_reached\", \"execute_fail\"} and data.get(\"checkoutUrl\"):\n handoff_required = True\n handoff_reason = \"manual_checkout_completion\"\n\n data[\"phase\"] = phase\n data[\"hint\"] = hint\n data[\"handoffRequired\"] = handoff_required\n data[\"handoffReason\"] = handoff_reason\n data[\"userMessage\"] = _build_user_message(data, phase)\n data[\"outcome\"] = {\"status\": outcome_status, \"hint\": data.get(\"outcome\", {}).get(\"hint\") or hint}\n return data\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9917,"content_sha256":"e20a5417914404385c51548a82a2a2375c49acede98b209b7ff29dc823faa110"},{"filename":"scripts/shopify/runtime.py","content":"#!/usr/bin/env python3\n\"\"\"Runtime helpers shared by Shopify automation scripts.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\nimport json\nimport os\nimport re\n\ntry: # pragma: no cover - optional dependency in some local environments\n from pypinyin import lazy_pinyin\nexcept Exception: # pragma: no cover\n lazy_pinyin = None\n\n\ndef parse_bool(value: Any, default: bool = False) -> bool:\n if value is None:\n return default\n if isinstance(value, bool):\n return value\n return str(value).strip().lower() in {\"1\", \"true\", \"yes\", \"y\", \"on\"}\n\n\ndef get_env(name: str, default: Any = None) -> Any:\n value = os.getenv(name)\n return value if value not in (None, \"\") else default\n\n\ndef resolve_path(value: Any) -> Path:\n expanded = os.path.expandvars(os.path.expanduser(str(value or \"\")))\n return Path(expanded)\n\n\ndef json_dumps(data: Any) -> str:\n return json.dumps(data, ensure_ascii=False, indent=2)\n\n\ndef normalize_text(value: Any) -> str:\n return re.sub(r\"\\s+\", \" \", str(value or \"\")).strip()\n\n\ndef _canonical_label(value: Any) -> str:\n return re.sub(r\"[^a-z0-9\\u4e00-\\u9fff]+\", \"\", normalize_text(value).lower())\n\n\ndef _first_non_empty(*values: Any) -> Any:\n for value in values:\n if value not in (None, \"\"):\n return value\n return None\n\n\ndef _split_full_name(value: Any) -> tuple[str, str]:\n parts = normalize_text(value).split(\" \", 1)\n if not parts or not parts[0]:\n return \"\", \"\"\n if len(parts) == 1:\n return parts[0], parts[0]\n return parts[0], parts[1]\n\n\nPHONE_PATTERN = re.compile(r\"^\\+?(?:\\d[\\d\\s().-]{6,}\\d|\\d{7,})$\")\nINLINE_PHONE_PATTERN = re.compile(\n r\"(?:电话(?:号码)?|手机号?|手机|phone|mobile|tel|telephone)\\s*[:: ]\\s*(\\+?(?:\\d[\\d\\s().-]{6,}\\d|\\d{7,}))\",\n re.IGNORECASE,\n)\nPOSTAL_PATTERN = re.compile(r\"^[A-Za-z0-9][A-Za-z0-9\\s-]{2,11}$\")\nADDRESS_HINT_PATTERN = re.compile(\n r\"(?:\\d|street|st\\b|road|rd\\b|ave\\b|avenue|boulevard|blvd|drive|dr\\b|lane|ln\\b|building|center|centre|district|\"\n r\"路|街|号|楼|室|苑|大厦|中心|区|镇|乡)\",\n re.IGNORECASE,\n)\nCHINA_PATTERN = re.compile(r\"中国|china|cn\", re.IGNORECASE)\nCHINA_STATE_PATTERN = re.compile(r\"(北京市|上海市|天津市|重庆市|[^省市]{2,12}省|[^区]{2,12}自治区|[^区]{2,12}特别行政区)\")\nCHINA_CITY_PATTERN = re.compile(r\"(.{2,12}?(?:市|州|盟|地区))\")\nCHINA_DISTRICT_PATTERN = re.compile(r\"(.{2,16}?(?:区|县|旗|镇|乡|街道|村))\")\nCJK_PATTERN = re.compile(r\"[\\u4e00-\\u9fff]+\")\n\nCHINA_REGION_ASCII: dict[str, str] = {\n \"北京市\": \"Beijing\",\n \"天津市\": \"Tianjin\",\n \"上海市\": \"Shanghai\",\n \"重庆市\": \"Chongqing\",\n \"河北省\": \"Hebei\",\n \"山西省\": \"Shanxi\",\n \"辽宁省\": \"Liaoning\",\n \"吉林省\": \"Jilin\",\n \"黑龙江省\": \"Heilongjiang\",\n \"江苏省\": \"Jiangsu\",\n \"浙江省\": \"Zhejiang\",\n \"安徽省\": \"Anhui\",\n \"福建省\": \"Fujian\",\n \"江西省\": \"Jiangxi\",\n \"山东省\": \"Shandong\",\n \"河南省\": \"Henan\",\n \"湖北省\": \"Hubei\",\n \"湖南省\": \"Hunan\",\n \"广东省\": \"Guangdong\",\n \"海南省\": \"Hainan\",\n \"四川省\": \"Sichuan\",\n \"贵州省\": \"Guizhou\",\n \"云南省\": \"Yunnan\",\n \"陕西省\": \"Shaanxi\",\n \"甘肃省\": \"Gansu\",\n \"青海省\": \"Qinghai\",\n \"台湾省\": \"Taiwan\",\n \"内蒙古自治区\": \"Inner Mongolia\",\n \"广西壮族自治区\": \"Guangxi\",\n \"西藏自治区\": \"Tibet\",\n \"宁夏回族自治区\": \"Ningxia\",\n \"新疆维吾尔自治区\": \"Xinjiang\",\n \"香港特别行政区\": \"Hong Kong\",\n \"澳门特别行政区\": \"Macau\",\n \"杭州市\": \"Hangzhou\",\n \"西湖区\": \"Xihu District\",\n \"黄龙国际中心\": \"Huanglong International Center\",\n \"浙数科技\": \"Zheshu Keji\",\n}\n\nCHINA_GENERAL_POSTAL_BY_REGION: dict[str, str] = {\n \"北京市\": \"100000\",\n \"天津市\": \"300000\",\n \"上海市\": \"200000\",\n \"重庆市\": \"400000\",\n \"河北省\": \"050000\",\n \"山西省\": \"030000\",\n \"辽宁省\": \"110000\",\n \"吉林省\": \"130000\",\n \"黑龙江省\": \"150000\",\n \"江苏省\": \"210000\",\n \"浙江省\": \"310000\",\n \"安徽省\": \"230000\",\n \"福建省\": \"350000\",\n \"江西省\": \"330000\",\n \"山东省\": \"250000\",\n \"河南省\": \"450000\",\n \"湖北省\": \"430000\",\n \"湖南省\": \"410000\",\n \"广东省\": \"510000\",\n \"海南省\": \"570000\",\n \"四川省\": \"610000\",\n \"贵州省\": \"550000\",\n \"云南省\": \"650000\",\n \"陕西省\": \"710000\",\n \"甘肃省\": \"730000\",\n \"青海省\": \"810000\",\n \"台湾省\": \"100000\",\n \"内蒙古自治区\": \"010000\",\n \"广西壮族自治区\": \"530000\",\n \"西藏自治区\": \"850000\",\n \"宁夏回族自治区\": \"750000\",\n \"新疆维吾尔自治区\": \"830000\",\n \"香港特别行政区\": \"999077\",\n \"澳门特别行政区\": \"999078\",\n}\n\nCHINA_REGION_SUFFIX_ASCII: tuple[tuple[str, str], ...] = (\n (\"特别行政区\", \" SAR\"),\n (\"自治区\", \" Autonomous Region\"),\n (\"自治州\", \" Prefecture\"),\n (\"街道\", \" Subdistrict\"),\n (\"地区\", \" Prefecture\"),\n (\"省\", \"\"),\n (\"市\", \"\"),\n (\"盟\", \" League\"),\n (\"区\", \" District\"),\n (\"县\", \" County\"),\n (\"旗\", \" Banner\"),\n (\"镇\", \" Town\"),\n (\"乡\", \" Township\"),\n (\"村\", \" Village\"),\n)\n\nCHINA_ADDRESS_PHRASE_ASCII: tuple[tuple[str, str], ...] = (\n (\"国际中心\", \" International Center \"),\n (\"国际广场\", \" International Plaza \"),\n (\"国际大厦\", \" International Tower \"),\n (\"科技园\", \" Tech Park \"),\n (\"工业园\", \" Industrial Park \"),\n (\"中心\", \" Center \"),\n (\"广场\", \" Plaza \"),\n (\"大厦\", \" Tower \"),\n (\"园区\", \" Park \"),\n (\"花园\", \" Garden \"),\n (\"小区\", \" Residential Compound \"),\n (\"科技\", \" Keji \"),\n (\"大楼\", \" Building \"),\n (\"号楼\", \" Building \"),\n (\"单元\", \" Unit \"),\n (\"室\", \" Room \"),\n (\"层\", \" Floor \"),\n (\"楼\", \" Floor \"),\n (\"路\", \" Road \"),\n (\"街\", \" Street \"),\n (\"道\", \" Road \"),\n (\"巷\", \" Lane \"),\n (\"弄\", \" Lane \"),\n (\"号\", \" No. \"),\n)\n\nSECTION_KEY_ALIASES: dict[str, tuple[str, ...]] = {\n \"name\": (\"name\", \"full_name\", \"recipient_name\", \"recipient\", \"receiver\", \"consignee\", \"contact_name\"),\n \"first_name\": (\"first_name\", \"given_name\", \"firstname\"),\n \"last_name\": (\"last_name\", \"family_name\", \"lastname\", \"surname\"),\n \"address1\": (\"address1\", \"line1\", \"street\", \"street1\"),\n \"address2\": (\"address2\", \"line2\", \"apartment\", \"suite\", \"unit\"),\n \"city\": (\"city\", \"city_name\", \"town\", \"municipality\"),\n \"state\": (\"state\", \"province\", \"region\", \"province_name\", \"state_name\"),\n \"postal\": (\"postal\", \"postal_code\", \"zip\", \"zipcode\", \"zip_code\"),\n \"country\": (\"country\", \"country_name\"),\n \"phone\": (\"phone\", \"phone_number\", \"mobile\", \"mobile_number\", \"tel\", \"telephone\"),\n}\n\nSECTION_BLOB_ALIASES: dict[str, tuple[str, ...]] = {\n \"address1\": (\"address\", \"full_address\", \"shipping_address\", \"delivery_address\", \"street_address\", \"raw_address\"),\n}\n\nLABEL_ALIASES: dict[str, tuple[str, ...]] = {\n \"name\": (\"name\", \"full name\", \"recipient\", \"recipient name\", \"receiver\", \"consignee\", \"contact name\", \"收件人\", \"姓名\", \"联系人\"),\n \"address1\": (\"address\", \"address line 1\", \"line1\", \"street\", \"shipping address\", \"delivery address\", \"收货地址\", \"地址\", \"详细地址\"),\n \"address2\": (\"address line 2\", \"line2\", \"apartment\", \"suite\", \"unit\", \"地址2\", \"楼层\", \"单元\"),\n \"city\": (\"city\", \"city name\", \"town\", \"城市\", \"市\"),\n \"state\": (\"state\", \"province\", \"region\", \"省份\", \"省\", \"州\", \"地区\"),\n \"postal\": (\"postal\", \"postal code\", \"zip\", \"zip code\", \"postcode\", \"邮编\", \"邮政编码\"),\n \"country\": (\"country\", \"国家\"),\n \"phone\": (\"phone\", \"mobile\", \"telephone\", \"tel\", \"电话号码\", \"手机号\", \"手机\", \"电话\"),\n}\n\nLABEL_ALIAS_TOKENS = {\n field: {_canonical_label(alias) for alias in aliases}\n for field, aliases in LABEL_ALIASES.items()\n}\n\n\ndef _looks_like_phone(value: Any) -> bool:\n text = normalize_text(value)\n if not text:\n return False\n return bool(PHONE_PATTERN.fullmatch(text))\n\n\ndef _normalize_phone(value: Any) -> str:\n text = normalize_text(value)\n if not text:\n return \"\"\n prefix = \"+\" if text.startswith(\"+\") else \"\"\n digits = re.sub(r\"\\D\", \"\", text)\n if len(digits) \u003c 7:\n return text\n return f\"{prefix}{digits}\" if prefix else digits\n\n\ndef _looks_like_postal(value: Any) -> bool:\n text = normalize_text(value)\n if not text:\n return False\n digits = re.sub(r\"\\D\", \"\", text)\n if digits and len(digits) > 8:\n return False\n return bool(POSTAL_PATTERN.fullmatch(text))\n\n\ndef _looks_like_address_line(value: Any) -> bool:\n text = normalize_text(value)\n if not text or len(text) \u003c 6:\n return False\n return bool(ADDRESS_HINT_PATTERN.search(text))\n\n\ndef _match_labeled_field(label: Any) -> str | None:\n token = _canonical_label(label)\n if not token:\n return None\n for field, aliases in LABEL_ALIAS_TOKENS.items():\n if token in aliases:\n return field\n return None\n\n\ndef _extract_labeled_values(section: dict[str, Any]) -> dict[str, str]:\n extracted: dict[str, str] = {}\n for raw_value in section.values():\n if not isinstance(raw_value, str):\n continue\n for piece in re.split(r\"[\\r\\n]+\", raw_value):\n line = normalize_text(piece)\n if not line:\n continue\n if \":\" in line or \":\" in line:\n label, content = re.split(r\"\\s*[::]\\s*\", line, maxsplit=1)\n field = _match_labeled_field(label)\n if field and content and field not in extracted:\n extracted[field] = normalize_text(content)\n continue\n phone_match = re.match(\n r\"^(电话(?:号码)?|手机号?|手机|phone|mobile|tel|telephone)\\s+(.+)$\",\n line,\n flags=re.IGNORECASE,\n )\n if phone_match and \"phone\" not in extracted:\n extracted[\"phone\"] = normalize_text(phone_match.group(2))\n return extracted\n\n\ndef _first_section_value(section: dict[str, Any], aliases: tuple[str, ...], labeled: dict[str, str] | None = None, labeled_key: str | None = None) -> str:\n for key in aliases:\n value = normalize_text(section.get(key))\n if value:\n return value\n if labeled and labeled_key:\n return normalize_text(labeled.get(labeled_key))\n return \"\"\n\n\ndef _extract_china_state_city(address_text: str) -> tuple[str, str]:\n text = normalize_text(address_text)\n if not text:\n return \"\", \"\"\n state_match = CHINA_STATE_PATTERN.search(text)\n state = normalize_text(state_match.group(1)) if state_match else \"\"\n tail = text[state_match.end() :] if state_match else text\n city_match = CHINA_CITY_PATTERN.search(tail)\n city = normalize_text(city_match.group(1)) if city_match else \"\"\n return state, city\n\n\ndef _contains_cjk(value: Any) -> bool:\n return bool(CJK_PATTERN.search(str(value or \"\")))\n\n\ndef _romanize_cjk_token(value: str, *, spaced: bool) -> str:\n text = normalize_text(value)\n if not text:\n return \"\"\n direct = CHINA_REGION_ASCII.get(text)\n if direct:\n return direct\n if lazy_pinyin is not None:\n pieces = [piece for piece in lazy_pinyin(text, errors=\"ignore\") if piece]\n if not pieces:\n return \"\"\n normalized = [piece.capitalize() for piece in pieces]\n return \" \".join(normalized) if spaced else \"\".join(normalized)\n return \"\"\n\n\ndef _romanize_region_name(value: Any) -> str:\n text = normalize_text(value)\n if not text:\n return \"\"\n direct = CHINA_REGION_ASCII.get(text)\n if direct:\n return direct\n for suffix, english_suffix in CHINA_REGION_SUFFIX_ASCII:\n if text.endswith(suffix) and len(text) > len(suffix):\n core = text[: -len(suffix)]\n core_ascii = _romanize_cjk_token(core, spaced=False) or _romanize_cjk_token(core, spaced=True)\n return normalize_text(f\"{core_ascii}{english_suffix}\")\n return _romanize_cjk_token(text, spaced=False) or _romanize_cjk_token(text, spaced=True)\n\n\ndef _romanize_address_line(value: Any, state: str = \"\", city: str = \"\") -> str:\n text = normalize_text(value)\n if not text:\n return \"\"\n if text.isascii():\n return text\n\n remainder = text\n for prefix in (normalize_text(state), normalize_text(city)):\n if prefix and remainder.startswith(prefix):\n remainder = remainder[len(prefix) :]\n remainder = normalize_text(remainder)\n\n for source, replacement in sorted(CHINA_REGION_ASCII.items(), key=lambda item: len(item[0]), reverse=True):\n if _contains_cjk(source):\n remainder = remainder.replace(source, f\" {replacement} \")\n remainder = re.sub(r\"([A-Za-z])座\", r\"Building \\1 \", remainder)\n remainder = re.sub(r\"([A-Za-z0-9]+)栋\", r\"Building \\1 \", remainder)\n remainder = re.sub(r\"([0-9]+)号楼\", r\"Building \\1 \", remainder)\n for source, replacement in CHINA_ADDRESS_PHRASE_ASCII:\n remainder = remainder.replace(source, replacement)\n\n tokens: list[str] = []\n for token in re.findall(r\"[\\u4e00-\\u9fff]+|[A-Za-z0-9]+|[^\\w\\s]+\", remainder):\n if not token or token.isspace():\n continue\n if CJK_PATTERN.fullmatch(token):\n ascii_token = _romanize_region_name(token) or _romanize_cjk_token(token, spaced=True)\n if ascii_token:\n tokens.append(ascii_token)\n continue\n tokens.append(token)\n\n ascii_text = \" \".join(tokens)\n ascii_text = re.sub(r\"\\s*,\\s*\", \", \", ascii_text)\n ascii_text = re.sub(r\"\\s+([/.#-])\", r\"\\1\", ascii_text)\n ascii_text = re.sub(r\"([/.#-])\\s+\", r\"\\1\", ascii_text)\n ascii_text = re.sub(r\"\\s+\", \" \", ascii_text).strip(\" ,\")\n return ascii_text\n\n\ndef _infer_china_general_postal(state: str, city: str) -> str:\n for value in (normalize_text(state), normalize_text(city)):\n if not value:\n continue\n if value in CHINA_GENERAL_POSTAL_BY_REGION:\n return CHINA_GENERAL_POSTAL_BY_REGION[value]\n return \"\"\n\n\ndef _stabilize_profile_section(section: dict[str, str]) -> dict[str, str]:\n result = {key: normalize_text(value) for key, value in section.items()}\n\n for field in (\"address1\", \"address2\", \"city\", \"state\"):\n value = result.get(field, \"\")\n inline_phone = INLINE_PHONE_PATTERN.search(value)\n if inline_phone and not result.get(\"phone\"):\n result[\"phone\"] = _normalize_phone(inline_phone.group(1))\n value = normalize_text(INLINE_PHONE_PATTERN.sub(\"\", value))\n result[field] = value\n\n for field in (\"city\", \"state\", \"address2\", \"address1\"):\n value = result.get(field, \"\")\n if not value:\n continue\n if _looks_like_phone(value):\n if not result.get(\"phone\"):\n result[\"phone\"] = _normalize_phone(value)\n result[field] = \"\"\n\n for field in (\"city\", \"state\", \"address2\"):\n value = result.get(field, \"\")\n if value and not result.get(\"postal\") and _looks_like_postal(value):\n result[\"postal\"] = value\n result[field] = \"\"\n\n if not result.get(\"address1\"):\n for field in (\"address2\", \"city\", \"state\"):\n value = result.get(field, \"\")\n if value and not _looks_like_phone(value) and not _looks_like_postal(value) and _looks_like_address_line(value):\n result[\"address1\"] = value\n result[field] = \"\"\n break\n\n china_like = any(\n CHINA_PATTERN.search(result.get(field, \"\"))\n for field in (\"country\", \"address1\", \"city\", \"state\")\n )\n if china_like and result.get(\"address1\"):\n guessed_state, guessed_city = _extract_china_state_city(result[\"address1\"])\n if guessed_state and not result.get(\"state\"):\n result[\"state\"] = guessed_state\n if guessed_city and not result.get(\"city\"):\n result[\"city\"] = guessed_city\n\n if china_like and result.get(\"state\") and not result.get(\"state_ascii\"):\n result[\"state_ascii\"] = _romanize_region_name(result[\"state\"])\n if china_like and result.get(\"city\") and not result.get(\"city_ascii\"):\n result[\"city_ascii\"] = _romanize_region_name(result[\"city\"])\n if china_like and result.get(\"address1\") and not result.get(\"address1_ascii\"):\n result[\"address1_ascii\"] = _romanize_address_line(\n result[\"address1\"],\n state=result.get(\"state\", \"\"),\n city=result.get(\"city\", \"\"),\n )\n district_match = CHINA_DISTRICT_PATTERN.search(result[\"address1\"])\n district = normalize_text(district_match.group(1)) if district_match else \"\"\n district_ascii = _romanize_region_name(district)\n if district_ascii and result[\"address1_ascii\"] and district_ascii not in result[\"address1_ascii\"]:\n result[\"address1_ascii\"] = normalize_text(f\"{result['address1_ascii']}, {district_ascii}\")\n if china_like and not result.get(\"postal\"):\n result[\"postal\"] = _infer_china_general_postal(result.get(\"state\", \"\"), result.get(\"city\", \"\"))\n\n result[\"phone\"] = _normalize_phone(result.get(\"phone\", \"\"))\n if not result.get(\"address1_ascii\") and result.get(\"address1\", \"\").isascii():\n result[\"address1_ascii\"] = result[\"address1\"]\n if not result.get(\"city_ascii\") and result.get(\"city\", \"\").isascii():\n result[\"city_ascii\"] = result[\"city\"]\n if not result.get(\"state_ascii\") and result.get(\"state\", \"\").isascii():\n result[\"state_ascii\"] = result[\"state\"]\n return result\n\n\ndef _any_present(section: dict[str, Any]) -> bool:\n meaningful_keys = (\"name\", \"first_name\", \"last_name\", \"address1\", \"address2\", \"city\", \"state\", \"postal\", \"phone\")\n return any(normalize_text(section.get(key)) for key in meaningful_keys)\n\n\ndef _normalize_profile_section(raw: dict[str, Any] | None, default_country: str = \"United States\") -> dict[str, str]:\n section = raw or {}\n labeled = _extract_labeled_values(section)\n first_name = _first_section_value(section, SECTION_KEY_ALIASES[\"first_name\"])\n last_name = _first_section_value(section, SECTION_KEY_ALIASES[\"last_name\"])\n full_name = _first_section_value(section, SECTION_KEY_ALIASES[\"name\"], labeled, \"name\")\n if full_name and not (first_name and last_name):\n fallback_first, fallback_last = _split_full_name(full_name)\n first_name = first_name or fallback_first\n last_name = last_name or fallback_last\n if not full_name and (first_name or last_name):\n full_name = \" \".join(part for part in [first_name, last_name] if part)\n normalized = {\n \"name\": full_name,\n \"first_name\": first_name,\n \"last_name\": last_name,\n \"address1\": _first_section_value(\n section,\n SECTION_KEY_ALIASES[\"address1\"],\n labeled,\n \"address1\",\n )\n or _first_section_value(section, SECTION_BLOB_ALIASES[\"address1\"]),\n \"address1_ascii\": normalize_text(\n section.get(\"address1_ascii\")\n or section.get(\"address1_latin\")\n or section.get(\"line1_ascii\")\n or section.get(\"street_ascii\")\n ),\n \"address2\": _first_section_value(section, SECTION_KEY_ALIASES[\"address2\"], labeled, \"address2\"),\n \"city\": _first_section_value(section, SECTION_KEY_ALIASES[\"city\"], labeled, \"city\"),\n \"city_ascii\": normalize_text(section.get(\"city_ascii\") or section.get(\"city_latin\")),\n \"state\": _first_section_value(section, SECTION_KEY_ALIASES[\"state\"], labeled, \"state\"),\n \"state_ascii\": normalize_text(section.get(\"state_ascii\") or section.get(\"state_latin\") or section.get(\"province_ascii\")),\n \"postal\": _first_section_value(section, SECTION_KEY_ALIASES[\"postal\"], labeled, \"postal\"),\n \"country\": _first_section_value(section, SECTION_KEY_ALIASES[\"country\"], labeled, \"country\") or normalize_text(default_country),\n \"phone\": _first_section_value(section, SECTION_KEY_ALIASES[\"phone\"], labeled, \"phone\"),\n }\n return _stabilize_profile_section(normalized)\n\n\n@dataclass\nclass Config:\n query: str\n candidate_urls_json: str\n mode: str\n secrets_path: Path\n out_dir: Path\n user_data_dir: Path | None\n fresh_profile: bool\n headed: bool\n use_vision: bool\n record_trace: bool\n record_video: bool\n browser_channel: str | None\n max_run_seconds: int\n max_total_usd: float\n keep_open_seconds: float\n action_delay_seconds: float\n manual_verification_timeout_seconds: int\n llm_provider: str\n browser_use_model: str\n openai_model: str\n max_steps: int\n proxy_server: str | None\n proxy_bypass: str | None\n proxy_username: str | None\n proxy_password: str | None\n confirm_delivery: bool\n confirm_legal_consent: bool\n resident_id_number: str | None\n record_order_on_success: bool\n order_label: str | None\n order_note: str | None\n order_currency: str | None\n order_store_dir: Path\n\n\ndef resolve_proxy_settings(args: Any) -> tuple[str | None, str | None, str | None, str | None]:\n proxy_server = _first_non_empty(\n getattr(args, \"proxy_server\", None),\n get_env(\"BROWSER_PROXY_URL\"),\n get_env(\"HTTPS_PROXY\"),\n get_env(\"https_proxy\"),\n get_env(\"HTTP_PROXY\"),\n get_env(\"http_proxy\"),\n get_env(\"ALL_PROXY\"),\n get_env(\"all_proxy\"),\n )\n proxy_bypass = _first_non_empty(\n getattr(args, \"proxy_bypass\", None),\n get_env(\"BROWSER_PROXY_BYPASS\"),\n get_env(\"NO_PROXY\"),\n get_env(\"no_proxy\"),\n )\n proxy_username = _first_non_empty(getattr(args, \"proxy_username\", None), get_env(\"BROWSER_PROXY_USERNAME\"))\n proxy_password = _first_non_empty(getattr(args, \"proxy_password\", None), get_env(\"BROWSER_PROXY_PASSWORD\"))\n return proxy_server, proxy_bypass, proxy_username, proxy_password\n\n\ndef load_secrets(path: Path) -> dict[str, Any]:\n if not path.exists():\n raise ValueError(f\"Secrets file not found: {path}\")\n data = json.loads(path.read_text(encoding=\"utf-8\"))\n payment = data.get(\"payment\") or {}\n email = normalize_text(payment.get(\"email\") or data.get(\"email\"))\n card_number = normalize_text(payment.get(\"card_number\"))\n exp = normalize_text(payment.get(\"exp\"))\n cvc = normalize_text(payment.get(\"cvc\"))\n delivery = _normalize_profile_section(data.get(\"delivery\") or data.get(\"shipping\"))\n legacy_billing = {\n \"name\": payment.get(\"name\"),\n \"address1\": payment.get(\"address1\"),\n \"address2\": payment.get(\"address2\"),\n \"city\": payment.get(\"city\"),\n \"state\": payment.get(\"state\"),\n \"postal\": payment.get(\"postal\"),\n \"country\": payment.get(\"country\"),\n \"phone\": payment.get(\"phone\"),\n }\n billing_raw = data.get(\"billing\") or {}\n billing_same_as_delivery = parse_bool(billing_raw.get(\"same_as_delivery\"), False)\n if not billing_raw and _any_present(delivery):\n billing_same_as_delivery = True\n billing_source = delivery if billing_same_as_delivery else (billing_raw or legacy_billing)\n billing = _normalize_profile_section(billing_source)\n postal = normalize_text(payment.get(\"postal\") or billing.get(\"postal\") or delivery.get(\"postal\"))\n country = normalize_text(payment.get(\"country\") or billing.get(\"country\") or delivery.get(\"country\") or \"United States\")\n cardholder_name = normalize_text(payment.get(\"name\") or billing.get(\"name\"))\n additional_information = data.get(\"additional_information\") or {}\n custom_fields = data.get(\"custom_fields\") or {}\n resident_id_number = normalize_text(\n _first_non_empty(\n payment.get(\"resident_id_number\"),\n payment.get(\"residentIdNumber\"),\n payment.get(\"resident_id\"),\n data.get(\"resident_id_number\"),\n data.get(\"residentIdNumber\"),\n data.get(\"resident_id\"),\n additional_information.get(\"resident_id_number\"),\n additional_information.get(\"residentIdNumber\"),\n additional_information.get(\"resident_id\"),\n custom_fields.get(\"resident_id_number\"),\n custom_fields.get(\"residentIdNumber\"),\n custom_fields.get(\"resident_id\"),\n )\n )\n missing = [\n name\n for name, value in [\n (\"payment.email\", email),\n (\"payment.card_number\", card_number),\n (\"payment.exp\", exp),\n (\"payment.cvc\", cvc),\n (\"payment.postal\", postal),\n (\"payment.country\", country),\n ]\n if not value\n ]\n if missing:\n raise ValueError(f\"Secrets file is missing required fields: {', '.join(missing)}\")\n return {\n \"email\": email,\n \"card_number\": card_number,\n \"card_exp\": exp,\n \"card_cvc\": cvc,\n \"card_postal\": postal,\n \"card_country\": country,\n \"card_name\": cardholder_name,\n \"resident_id_number\": resident_id_number,\n \"delivery\": delivery,\n \"billing\": billing,\n \"billing_same_as_delivery\": billing_same_as_delivery,\n }\n\n\ndef build_sensitive_data(secrets: dict[str, Any]) -> dict[str, str]:\n delivery = secrets.get(\"delivery\") or {}\n billing = secrets.get(\"billing\") or {}\n return {\n \"guest_email\": secrets[\"email\"],\n \"card_number\": secrets[\"card_number\"],\n \"card_exp\": secrets[\"card_exp\"],\n \"card_cvc\": secrets[\"card_cvc\"],\n \"card_postal\": secrets[\"card_postal\"],\n \"card_country\": secrets[\"card_country\"],\n \"card_name\": secrets.get(\"card_name\", \"\"),\n \"resident_id_number\": secrets.get(\"resident_id_number\", \"\"),\n \"delivery_name\": delivery.get(\"name\", \"\"),\n \"delivery_first_name\": delivery.get(\"first_name\", \"\"),\n \"delivery_last_name\": delivery.get(\"last_name\", \"\"),\n \"delivery_address1\": delivery.get(\"address1\", \"\"),\n \"delivery_address1_ascii\": delivery.get(\"address1_ascii\", \"\"),\n \"delivery_address2\": delivery.get(\"address2\", \"\"),\n \"delivery_city\": delivery.get(\"city\", \"\"),\n \"delivery_city_ascii\": delivery.get(\"city_ascii\", \"\"),\n \"delivery_state\": delivery.get(\"state\", \"\"),\n \"delivery_state_ascii\": delivery.get(\"state_ascii\", \"\"),\n \"delivery_postal\": delivery.get(\"postal\", \"\"),\n \"delivery_country\": delivery.get(\"country\", \"\"),\n \"delivery_phone\": delivery.get(\"phone\", \"\"),\n \"billing_name\": billing.get(\"name\", \"\"),\n \"billing_first_name\": billing.get(\"first_name\", \"\"),\n \"billing_last_name\": billing.get(\"last_name\", \"\"),\n \"billing_address1\": billing.get(\"address1\", \"\"),\n \"billing_address1_ascii\": billing.get(\"address1_ascii\", \"\"),\n \"billing_address2\": billing.get(\"address2\", \"\"),\n \"billing_city\": billing.get(\"city\", \"\"),\n \"billing_city_ascii\": billing.get(\"city_ascii\", \"\"),\n \"billing_state\": billing.get(\"state\", \"\"),\n \"billing_state_ascii\": billing.get(\"state_ascii\", \"\"),\n \"billing_postal\": billing.get(\"postal\", \"\"),\n \"billing_country\": billing.get(\"country\", \"\"),\n \"billing_phone\": billing.get(\"phone\", \"\"),\n \"billing_same_as_delivery\": \"true\" if secrets.get(\"billing_same_as_delivery\") else \"\",\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27421,"content_sha256":"f157f07b7f2dd91548e63cdd0ed1c2e432728a05ca56a58c34f18a3ea50dc4ab"},{"filename":"scripts/shopify/security.py","content":"#!/usr/bin/env python3\n\"\"\"Shared text heuristics for security and checkout states.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\nimport re\n\n\ndef normalize_text(value: Any) -> str:\n return re.sub(r\"\\s+\", \" \", str(value or \"\")).strip()\n\n\ndef digits(value: Any) -> str:\n return re.sub(r\"\\D+\", \"\", str(value or \"\"))\n\n\ndef money_from_text(text: str) -> float | None:\n patterns = (\n r\"(?:US\\$|CA\\$|CAD\\$|USD\\$|\\$)\\s*([0-9]+(?:\\.[0-9]{1,2})?)\",\n r\"total\\s+([0-9]+(?:\\.[0-9]{1,2})?)\",\n )\n for pattern in patterns:\n match = re.search(pattern, text or \"\", re.I)\n if not match:\n continue\n try:\n return float(match.group(1))\n except ValueError:\n continue\n return None\n\n\ndef looks_like_security_verification(text: str) -> bool:\n return bool(\n re.search(\n r\"performing security verification|security service to protect against malicious bots|\"\n r\"verify you are human|checking your browser|cloudflare|turnstile|captcha|challenge\",\n text or \"\",\n re.I,\n )\n )\n\n\ndef looks_like_checkout_success(text: str, url: str) -> bool:\n haystack = f\"{text} {url}\".lower()\n return bool(\n re.search(\n r\"thank you|receipt|order confirmed|payment successful|gift card will be sent|complete\",\n haystack,\n )\n )\n\n\ndef looks_like_checkout_failure(text: str) -> bool:\n return bool(\n re.search(\n r\"payment failed|declined|card was declined|authentication failed|could not be completed|invalid postal\",\n text or \"\",\n re.I,\n )\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1661,"content_sha256":"e062b14905c9e6d53ec4895a417fb0b66254f1f1abe692da882d96cfb3994743"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"FluxA Agentic Checkout","type":"text"}]},{"type":"paragraph","content":[{"text":"Skill version: 0.4.0","type":"text","marks":[{"type":"strong"}]},{"text":" | ","type":"text"},{"text":"Product surface: deterministic checkout automation + explicit human handoff","type":"text","marks":[{"type":"strong"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill as the release-grade operator runbook for checkout automation. It accepts arbitrary entry links, attempts the currently implemented automation routes, and produces structured results that tell the next agent or human whether the checkout is ready, partially filled, blocked by verification, or requires manual takeover.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to do checkout for users","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Check whether the URL supports automated checkout","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check whether the user has provided a specific product URL. If not, ask the user for the exact product link they want to buy.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check whether the link falls within the currently supported automation scope. This skill is positioned as a general checkout automation skill and should determine whether there is an executable automation path for the given link. If the merchant or checkout flow is outside the currently executable scope, the skill should clearly tell the user that they need to complete the purchase manually and provide a direct link they can open.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the link is supported, tell the user: \"This item supports automated checkout. We can help autofill the checkout information and guide you through the purchase. I’m starting the checkout automation now.\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Check the checkout skill execution environment","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check the environment. The skill must run in an environment that supports Playwright, and Chromium must be installed. Check whether ","type":"text"},{"text":"python-dotenv","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"socksio","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"playwright","type":"text","marks":[{"type":"code_inline"}]},{"text":", and related dependencies are already installed. If they are missing, install them first. See the ","type":"text"},{"text":"Environment setup","type":"text","marks":[{"type":"code_inline"}]},{"text":" section for the setup steps.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Before installing the environment, tell the user: \"To complete the automated checkout, we need to install a few required tools and the browser runtime. This may take a few minutes. I’m starting the setup now.\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Collect the user's checkout information and generate the profile","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read the ","type":"text"},{"text":"Collect the user's shipping information","type":"text","marks":[{"type":"code_inline"}]},{"text":" section and check whether the user already has a JSON file containing payment, delivery, and billing information. If not, guide the user to provide that information.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the information already exists, confirm the shipping address for this purchase with the user again.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Run the checkout script in preview and execute modes","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Before starting, tell the user: \"I’m starting the checkout automation now. I’ll begin in preview mode so I can autofill the checkout information without submitting the order.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the user-provided ","type":"text"},{"text":"entry-url","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the generated profile JSON file to run ","type":"text"},{"text":"checkout_playwright_handoff.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"preview","type":"text","marks":[{"type":"code_inline"}]},{"text":" mode first, so you can confirm that the automation flow runs correctly and fills the required information.","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/checkout_playwright_handoff.py \\\n --entry-url \"https://shophomeplace.myshopify.com/products/gift-card\" \\\n --mode preview \\\n --secrets-path /data/workspace/.clawdbot/credentials/real_card.json \\\n --resident-id-number \"\u003crequired only when the merchant asks for it>\" \\\n --out-dir artifacts/checkout-preview \\\n --headless \\\n --record-video","type":"text"}]},{"type":"ordered_list","attrs":{"order":3,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"preview","type":"text","marks":[{"type":"code_inline"}]},{"text":" succeeds, send the screenshots and video to the user so they can review the automated checkout preview, and tell the user: \"The automated checkout preview completed successfully, and the checkout information has been filled in. Please confirm that the delivery information is correct. If everything looks right, we can proceed with the final checkout step.\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not delete screenshots, traces, videos, or result JSON files under the local ","type":"text"},{"text":"artifacts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory by default. They are part of the audit trail and manual handoff record for checkout continuation. Only clean up local artifacts if the user explicitly asks for cleanup, or explicitly asks to delete sensitive files among them.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the page contains a required checkbox for legal consent, privacy authorization, cross-border transfer, or other local regulatory requirements, do not check it on the user's behalf by default. First ask for the user's explicit consent in natural language. Only after the user clearly agrees should you add ","type":"text"},{"text":"--confirm-legal-consent","type":"text","marks":[{"type":"code_inline"}]},{"text":" to the command.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the user confirms that the delivery information is correct, run ","type":"text"},{"text":"checkout_playwright_handoff.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"execute","type":"text","marks":[{"type":"code_inline"}]},{"text":" mode for the final checkout attempt. When using the standard unified profile that includes ","type":"text"},{"text":"delivery","type":"text","marks":[{"type":"code_inline"}]},{"text":", you should usually add ","type":"text"},{"text":"--confirm-delivery","type":"text","marks":[{"type":"code_inline"}]},{"text":"; otherwise the script will refuse to run.","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/checkout_playwright_handoff.py \\\n --entry-url \"https://gracie-designs.myshopify.com/products/gift-card\" \\\n --mode execute \\\n --secrets-path \"$HOME/.clawdbot/credentials/real_card.json\" \\\n --confirm-delivery \\\n --confirm-legal-consent \\\n --order-label \"Gracie Designs Gift Card\" \\\n --order-currency USD","type":"text"}]},{"type":"ordered_list","attrs":{"order":6,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When the flow is unsupported or blocked, speak to the user in plain language. Do not answer with internal labels like ","type":"text"},{"text":"unsupported_provider","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"needs_manual_verification","type":"text","marks":[{"type":"code_inline"}]},{"text":" only. If the checkout page is already open but automation still cannot safely continue, return the live product or checkout link immediately so the user can finish manually. Preferred wording:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"This merchant's current checkout flow is not yet within the validated automation scope of FluxA Agentic Checkout, so you will need to complete this purchase manually for now.\nYou can open the link below to continue the purchase:\n\u003cproduct-or-checkout-url>","type":"text"}]},{"type":"ordered_list","attrs":{"order":7,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Query the local paid-order ledger when needed.","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/order_manager.py get --order-id CHK-1774977424895\npython3 scripts/order_manager.py list --limit 20\npython3 scripts/order_manager.py search --keyword \"gift card\"\npython3 scripts/order_manager.py summary --days 30","type":"text"}]},{"type":"ordered_list","attrs":{"order":8,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Judge the next action from the JSON result, not from transient browser logs. Use ","type":"text"},{"text":"phase","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"provider","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"handoffRequired","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"handoffReason","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"contactFilled","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"deliveryFilled","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"billingIdentityFilled","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"postalFilled","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"paymentFieldVerification","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"legalConsentChecked","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"filledCheckoutScreenshot","type":"text","marks":[{"type":"code_inline"}]},{"text":". If ","type":"text"},{"text":"handoffRequired=true","type":"text","marks":[{"type":"code_inline"}]},{"text":", stop automation and pass the active context to a human operator instead of inventing a fallback flow.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Product Capabilities","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Capability","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it does now","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"When to use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Checkout Routing","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Accepts entry links and chooses the validated checkout route automatically","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The caller already has a product, cart, or checkout URL","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deterministic Filling","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fills supported contact, delivery, billing identity, postal, and card fields on currently supported surfaces","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The caller wants a stable, replayable automation path instead of free-form browsing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Profile Setup","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Collects payment, delivery, and billing details into one reusable JSON credential file","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The caller needs a repeatable setup flow before preview or execute","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Operator Handoff","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Emits structured handoff states instead of faking unsupported automation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verification or merchant-specific steps block safe continuation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Artifacts & State","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Saves JSON results, screenshots, traces, and optional video for inspection","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The caller needs auditability, debugging, or clean manual continuation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Order Management","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stores successful paid checkout records inside the skill and exposes lookup commands","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The caller needs a local ledger of paid orders after execute-mode success","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Current Implementation Status","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this section as the product truth. Do not claim support beyond it.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Implemented now","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Standard storefront checkout navigation via Playwright","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Accepts product, collection, cart, or direct checkout pages on currently validated storefront routes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Navigates into checkout with deterministic actions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fills contact and delivery fields when the checkout presents a shipping step","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Supports ","type":"text"},{"text":"preview","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"execute","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hosted checkout field filling","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detects direct hosted checkout pages and common embedded payment surfaces on currently validated routes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fills visible identity, postal, and card fields","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Checkout profile loading","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reads a single JSON file that contains ","type":"text"},{"text":"payment","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"delivery","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"billing","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Remains backward-compatible with the older card-only JSON shape","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Structured outcome reporting","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Returns ","type":"text"},{"text":"phase","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"provider","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"handoffRequired","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"handoffReason","type":"text","marks":[{"type":"code_inline"}]},{"text":", and completion hints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Produces screenshots, traces, and JSON payloads suitable for a human or downstream agent","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local paid-order ledger","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Successful ","type":"text"},{"text":"execute","type":"text","marks":[{"type":"code_inline"}]},{"text":" runs can be recorded automatically into ","type":"text"},{"text":"data/paid_orders/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/order_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" supports ","type":"text"},{"text":"get","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"list","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"search","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"summary","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Implemented but requires human intervention in some states","type":"text"}]},{"type":"paragraph","content":[{"text":"These are expected handoff points, not bugs to paper over:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CAPTCHA","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cloudflare or anti-bot verification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OTP / SMS / email verification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Issuer 3DS authentication","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Merchant login walls","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unsupported checkout widgets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Merchant-specific post-submit review states","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When these appear, stop and return the handoff state. Do not improvise a best-effort checkout flow.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Not implemented or not claimed","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Universal support for all ecommerce platforms","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Arbitrary pre-checkout storefront traversal on every merchant","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full support for heavily customized storefront themes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CAPTCHA / OTP / 3DS bypass","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"First-class adapters for Adyen, Braintree, and other providers not yet validated","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Contract","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill is an execution and handoff product, not a recommendation report.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the route is supported and the run reaches the expected checkpoint, return the structured JSON result and the key artifact paths","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not delete local ","type":"text"},{"text":"artifacts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" by default; keep them available for user review, debugging, and manual continuation unless the user explicitly asks for cleanup","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the route is blocked by verification or unsupported flow, return the handoff state clearly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the provider is outside the validated surface list, report that explicitly instead of claiming partial support","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If a delivery step is present, surface whether it was filled via ","type":"text"},{"text":"deliveryFilled","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the payment iframe visually masks values in screenshots, rely on ","type":"text"},{"text":"paymentFieldVerification","type":"text","marks":[{"type":"code_inline"}]},{"text":" for card-field confirmation instead of claiming the fields are empty from the screenshot alone","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the page shows a required legal/privacy consent checkbox, only auto-check it after the user explicitly agrees; surface the result via ","type":"text"},{"text":"legalConsentChecked","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"execute","type":"text","marks":[{"type":"code_inline"}]},{"text":" reaches success and order recording is enabled, include ","type":"text"},{"text":"orderRecorded","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"orderId","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"orderPath","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"orderStorageDir","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the final result","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If checkout succeeds but ledger persistence fails, surface ","type":"text"},{"text":"orderRecordError","type":"text","marks":[{"type":"code_inline"}]},{"text":" without pretending the order was recorded","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Preview runs and non-success execute runs should not be recorded as paid orders","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Current implementation scope: Read ","type":"text"},{"text":"references/current-capabilities.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/current-capabilities.md","title":null}}]},{"text":" before claiming support for a merchant or payment surface.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Planned expansion: Read ","type":"text"},{"text":"references/roadmap.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/roadmap.md","title":null}}]},{"text":" when the user asks what is not implemented yet or what the team plans to ship next.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scripts","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/checkout_playwright_handoff.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" General checkout entrypoint with automatic route selection, structured handoff signals, and optional paid-order recording on execute success.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/demo_execute_headed.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" One-command execute-mode demo wrapper with ","type":"text"},{"text":"--headed","type":"text","marks":[{"type":"code_inline"}]},{"text":", default demo URL, and visible browser hold-open time.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/setup_checkout_profile.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" Interactive setup flow that collects payment, delivery, and billing details into one reusable JSON profile.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/order_manager.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" Local paid-order query CLI for ","type":"text"},{"text":"get","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"list","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"search","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"summary","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/order_store.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" Filesystem-backed order ledger used by the checkout skill.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/shopify/","type":"text","marks":[{"type":"code_inline"}]},{"text":" Bundled Playwright helpers for the currently validated checkout navigation routes and shared payment adapters.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Environment setup","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/setup_checkout_skill.py \\\n --profile-output \"$HOME/.clawdbot/credentials/real_card.json\"","type":"text"}]},{"type":"paragraph","content":[{"text":"This one command prepares the Python runtime, installs Playwright Chromium, and then starts the interactive profile setup flow.","type":"text"}]},{"type":"paragraph","content":[{"text":"Default checkout runs now use a ","type":"text"},{"text":"600","type":"text","marks":[{"type":"code_inline"}]},{"text":" second timeout so slow stores are less likely to be cut off mid-run. When automation is blocked, reply to the user in a customer-service tone and either ask for the missing checkout information or immediately return the live checkout link for manual completion.","type":"text"}]},{"type":"paragraph","content":[{"text":"Standard local setup:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 -m venv .venv\n. .venv/bin/activate\npython -m pip install -r requirements.txt\npython -m playwright install chromium","type":"text"}]},{"type":"paragraph","content":[{"text":"Docker / OpenClaw setup:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"apt-get update && apt-get install -y python3-pip python3-venv xauth\npython3 -m pip install --break-system-packages playwright python-dotenv socksio\npython3 -m playwright install --with-deps chromium\npython3 -m playwright install chromium","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the final ","type":"text"},{"text":"python3 -m playwright install chromium","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the same user context that will run the checkout command. If the container installs dependencies as ","type":"text"},{"text":"root","type":"text","marks":[{"type":"code_inline"}]},{"text":" but OpenClaw executes as ","type":"text"},{"text":"node","type":"text","marks":[{"type":"code_inline"}]},{"text":", run that final install again as ","type":"text"},{"text":"node","type":"text","marks":[{"type":"code_inline"}]},{"text":" so the browser cache exists under the runtime user's home directory.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Collect the user's shipping information","type":"text"}]},{"type":"paragraph","content":[{"text":"Run the update script:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/setup_checkout_profile.py \\\n --output \"$HOME/.clawdbot/credentials/real_card.json\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Before updating it, first check whether this file already contains the user's collected information. The generated profile JSON uses this shape:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"payment\": {\n \"email\": \"[email protected]\",\n \"card_number\": \"4242424242424242\",\n \"exp\": \"12/34\",\n \"cvc\": \"123\",\n \"postal\": \"10001\",\n \"country\": \"United States\",\n \"name\": \"Cardholder Name\"\n },\n \"delivery\": {\n \"name\": \"Test Buyer\",\n \"first_name\": \"Test\",\n \"last_name\": \"Buyer\",\n \"address1\": \"123 Main St\",\n \"address1_ascii\": \"123 Main St\",\n \"address2\": \"Apt 2\",\n \"city\": \"New York\",\n \"city_ascii\": \"New York\",\n \"state\": \"NY\",\n \"state_ascii\": \"NY\",\n \"postal\": \"10001\",\n \"country\": \"United States\",\n \"phone\": \"2125550100\"\n },\n \"billing\": {\n \"same_as_delivery\": false,\n \"name\": \"Billing Person\",\n \"first_name\": \"Billing\",\n \"last_name\": \"Person\",\n \"address1\": \"456 Billing Ave\",\n \"address1_ascii\": \"456 Billing Ave\",\n \"address2\": \"Suite 5\",\n \"city\": \"New York\",\n \"city_ascii\": \"New York\",\n \"state\": \"NY\",\n \"state_ascii\": \"NY\",\n \"postal\": \"10001\",\n \"country\": \"United States\",\n \"phone\": \"2125550101\"\n },\n \"additional_information\": {\n \"resident_id_number\": \"540531196711167179\"\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"additional_information.resident_id_number","type":"text","marks":[{"type":"code_inline"}]},{"text":" is optional. When the merchant requires it only for one checkout, you can also pass it at runtime with ","type":"text"},{"text":"--resident-id-number","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"RESIDENT_ID_NUMBER","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"If it does not, ask the user for the relevant information and then run the script above to update it. For addresses in China, if the user does not separately provide an English or Latin version, the skill should first generate ","type":"text"},{"text":"address1_ascii","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"city_ascii","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"state_ascii","type":"text","marks":[{"type":"code_inline"}]},{"text":" automatically. If the postal code is missing, it should first fall back to a province-level generic postal code before continuing the checkout. Only if the page still rejects the address after automatic inference should the skill go back to the user and ask for more precise information. If any of these are missing, ask the user directly in plain language:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Checkout email","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery recipient name","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery country or region","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery province or state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery city","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery address line 1","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery phone number","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Delivery postal code when the merchant requires it","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the merchant checkout rejects native script: an English / Latin version of the delivery address, city, and province","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Whether billing address is the same as delivery","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If billing is different: billing name, billing country or region, billing province or state, billing city, billing address line 1, billing phone, billing postal code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If billing is different and the merchant checkout rejects native script: an English / Latin version of the billing address, city, and province","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"fluxa agentic checkout","author":"@skillopedia","source":{"stars":3,"repo_name":"fluxa-ai-wallet-mcp","origin_url":"https://github.com/fluxa-agent-payment/fluxa-ai-wallet-mcp/blob/HEAD/skills/agentic-checkout/SKILL.md","repo_owner":"fluxa-agent-payment","body_sha256":"68d522b31db029a7bd62fc9e057c40648a86adc6f38d20dfb934d7bd1c7064f5","cluster_key":"3d3ce1fc5b882f17ddd7137a3f4c2b615985ed4159942ec87f30a83d0aafa270","clean_bundle":{"format":"clean-skill-bundle-v1","source":"fluxa-agent-payment/fluxa-ai-wallet-mcp/skills/agentic-checkout/SKILL.md","attachments":[{"id":"5ecc9a72-80e2-5367-82f7-dbb40e44fca2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5ecc9a72-80e2-5367-82f7-dbb40e44fca2/attachment.yaml","path":"agents/openai.yaml","size":310,"sha256":"33960620e65fba704e1688fc9ef78e408d70efc48e11f847c8b0eb2662a8e65d","contentType":"application/yaml; charset=utf-8"},{"id":"d4c06ee1-a797-5d51-87fc-c2234121d5f7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4c06ee1-a797-5d51-87fc-c2234121d5f7/attachment","path":"data/paid_orders/.gitignore","size":14,"sha256":"240a3e0d37d2e86b614063f5347eb02d4f99ca6c254de6b82871ff8d95532a7d","contentType":"text/plain; charset=utf-8"},{"id":"19bf19c3-e8b8-5fb2-b026-3be309bc4438","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/19bf19c3-e8b8-5fb2-b026-3be309bc4438/attachment.md","path":"references/current-capabilities.md","size":2613,"sha256":"e5a62a9e8b53b2b79964ce4440d5f093d608b5cc651ecd8e3559383467d66c3f","contentType":"text/markdown; charset=utf-8"},{"id":"f75dfdaa-069c-5941-83ee-c2449f8126a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f75dfdaa-069c-5941-83ee-c2449f8126a9/attachment.md","path":"references/roadmap.md","size":1143,"sha256":"2a1f973cfeae0ae7041ca82fab887a3e4c75510eb8e0f243debaa1b16bb1625d","contentType":"text/markdown; charset=utf-8"},{"id":"2abc2e13-4832-583c-8539-a77738047932","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2abc2e13-4832-583c-8539-a77738047932/attachment.txt","path":"requirements.txt","size":42,"sha256":"a1d352f688dcc437d0d45a9eb0e62f45fc4df8cf22cebf53e0e73f2774a2d89a","contentType":"text/plain; charset=utf-8"},{"id":"652e5602-f0a6-54f5-a128-caa49eae6048","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/652e5602-f0a6-54f5-a128-caa49eae6048/attachment.py","path":"scripts/checkout_playwright_handoff.py","size":23470,"sha256":"25ee6e48417c881b0af6d8a390bf03e86dfc326d5d1b16ebf0c6e63d5fe8f441","contentType":"text/x-python; charset=utf-8"},{"id":"f1774652-4179-5aa5-9020-2f62a26dbe20","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f1774652-4179-5aa5-9020-2f62a26dbe20/attachment.py","path":"scripts/demo_execute_headed.py","size":3481,"sha256":"f34f80d86f62407fa91c5d383b4df55ac6e0bd39c3d14e9477c4f064ffd3c4e4","contentType":"text/x-python; charset=utf-8"},{"id":"467752a4-c313-5fb0-bd5c-e4c566aac915","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/467752a4-c313-5fb0-bd5c-e4c566aac915/attachment.py","path":"scripts/order_manager.py","size":6046,"sha256":"d7e85c11f475c4c9b14549cd4b4b84c4a01cf6a232923bd64d0655d03e22877c","contentType":"text/x-python; charset=utf-8"},{"id":"104e21c0-b7f0-5cee-82ff-0ca3494a0fff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/104e21c0-b7f0-5cee-82ff-0ca3494a0fff/attachment.py","path":"scripts/order_store.py","size":11381,"sha256":"d4f5b248ed368f14ef1827bfdef3eb1fd3da9d28b1c406b3b82e1e463da359dd","contentType":"text/x-python; charset=utf-8"},{"id":"b69b972d-9972-542d-a69e-a9ac3e6f0646","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b69b972d-9972-542d-a69e-a9ac3e6f0646/attachment.py","path":"scripts/setup_checkout_profile.py","size":7294,"sha256":"e8a2d761dfd2adb1d8238ffda3af4e87f7630643f9164a78ae5bfeaafecfd60e","contentType":"text/x-python; charset=utf-8"},{"id":"6298cc81-2a16-554b-a0c3-e280acfa551c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6298cc81-2a16-554b-a0c3-e280acfa551c/attachment.py","path":"scripts/setup_checkout_skill.py","size":3640,"sha256":"7152b7882506e4aa55caac59a5979a7ebd040870bae46e04d291f1390d3eb21e","contentType":"text/x-python; charset=utf-8"},{"id":"9eed5fd7-2385-55f6-8b37-9da73e10c0be","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9eed5fd7-2385-55f6-8b37-9da73e10c0be/attachment.py","path":"scripts/shopify/__init__.py","size":53,"sha256":"fb0558977783dd0b76a8fdb56fd1bce106ac698b37976fbe95e29abe01375f2e","contentType":"text/x-python; charset=utf-8"},{"id":"69a633e0-7bf7-5262-b287-bc4d60d6f2fb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/69a633e0-7bf7-5262-b287-bc4d60d6f2fb/attachment.py","path":"scripts/shopify/adapters/__init__.py","size":46,"sha256":"1699226fd93cb6a20c9fd4b872f30679b608fcda8821ce1424f9948f24c474aa","contentType":"text/x-python; charset=utf-8"},{"id":"4cc66a2a-b884-554a-982b-50c43331fd70","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4cc66a2a-b884-554a-982b-50c43331fd70/attachment.py","path":"scripts/shopify/adapters/common.py","size":13622,"sha256":"d45ef6d4a911182ce214bc13d33453335fc0ed778ad27a777d7f0a72992b39dd","contentType":"text/x-python; charset=utf-8"},{"id":"37938d42-b9f8-55ac-8ff9-bd7f726f7066","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/37938d42-b9f8-55ac-8ff9-bd7f726f7066/attachment.py","path":"scripts/shopify/adapters/shop_pay_card.py","size":5892,"sha256":"a98c1e527b0547778c5f4183edf29a827c7e80efaa4ba9a8c33f0b6a3103cf1f","contentType":"text/x-python; charset=utf-8"},{"id":"93c2e61c-a436-52e6-854a-1873488bf573","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/93c2e61c-a436-52e6-854a-1873488bf573/attachment.py","path":"scripts/shopify/adapters/shopify_checkout_card.py","size":15793,"sha256":"6fd3ebc81488203093ce408537f745f67b0e500fc6ed335cf5732b5790e5b6c8","contentType":"text/x-python; charset=utf-8"},{"id":"15eddc12-ba15-5419-a3f9-a15bdde008ed","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/15eddc12-ba15-5419-a3f9-a15bdde008ed/attachment.py","path":"scripts/shopify/adapters/stripe_hosted.py","size":16127,"sha256":"ab9365108cbaf417dcda21506ba27c937ef15cbbc33fb6b208f78799715c54a8","contentType":"text/x-python; charset=utf-8"},{"id":"557f3954-b367-587e-9077-d68ac547e709","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/557f3954-b367-587e-9077-d68ac547e709/attachment.py","path":"scripts/shopify/candidates.py","size":6841,"sha256":"b1e7c8017ebf87cf3f1f74a28e7b2f5d5957709a23f8b971f5f8f8e0b8d46dbf","contentType":"text/x-python; charset=utf-8"},{"id":"89e0bc2e-2442-59ff-b55e-68f463424472","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/89e0bc2e-2442-59ff-b55e-68f463424472/attachment.py","path":"scripts/shopify/checkout.py","size":54411,"sha256":"41a52e5f96c738b8bd699f62bab907e69976062159648df2ae68811d7a671fd0","contentType":"text/x-python; charset=utf-8"},{"id":"596b9da2-425a-5367-8d96-778307c16a49","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/596b9da2-425a-5367-8d96-778307c16a49/attachment.py","path":"scripts/shopify/navigation.py","size":55423,"sha256":"944937e991c25ac69b6d93f030251f92cb8938a9c7134291b6f4393f277bcf00","contentType":"text/x-python; charset=utf-8"},{"id":"cd0c381c-39e8-59d1-a949-73c9d0c78d75","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cd0c381c-39e8-59d1-a949-73c9d0c78d75/attachment.py","path":"scripts/shopify/providers.py","size":3280,"sha256":"49e26f4ed756c0f56ac71bdc8709b12ed4df1f7f8121321dc28c2e5b72d7cb3f","contentType":"text/x-python; charset=utf-8"},{"id":"cd7530d2-887e-51fa-9dfc-92ac98c3c045","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cd7530d2-887e-51fa-9dfc-92ac98c3c045/attachment.py","path":"scripts/shopify/results.py","size":9917,"sha256":"e20a5417914404385c51548a82a2a2375c49acede98b209b7ff29dc823faa110","contentType":"text/x-python; charset=utf-8"},{"id":"0073793b-8419-501c-8b35-caae6a05501d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0073793b-8419-501c-8b35-caae6a05501d/attachment.py","path":"scripts/shopify/runtime.py","size":27421,"sha256":"f157f07b7f2dd91548e63cdd0ed1c2e432728a05ca56a58c34f18a3ea50dc4ab","contentType":"text/x-python; charset=utf-8"},{"id":"a4faedf0-b8e6-57fb-8659-0a35839ff186","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4faedf0-b8e6-57fb-8659-0a35839ff186/attachment.py","path":"scripts/shopify/security.py","size":1661,"sha256":"e062b14905c9e6d53ec4895a417fb0b66254f1f1abe692da882d96cfb3994743","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"5f484c1538dd823659ef024b332cf6ea8d74bec4a62880789a6d49ac7b1ce35c","attachment_count":24,"text_attachments":23,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":1,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/agentic-checkout/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"clean-skills-v1","description":"FluxA Agentic Checkout is a general-purpose checkout automation and human handoff runbook. Use it when an AI agent needs to open a product or checkout link, attempt deterministic Playwright checkout on currently supported surfaces, autofill contact, delivery, card, and billing fields, and stop in a clean handoff state when CAPTCHA, Cloudflare, OTP, 3DS, unsupported merchants, or store-specific flows require a human operator to finish the purchase."}},"renderedAt":1782988169226}

FluxA Agentic Checkout Skill version: 0.4.0 | Product surface: deterministic checkout automation + explicit human handoff Overview Use this skill as the release-grade operator runbook for checkout automation. It accepts arbitrary entry links, attempts the currently implemented automation routes, and produces structured results that tell the next agent or human whether the checkout is ready, partially filled, blocked by verification, or requires manual takeover. How to do checkout for users Step 1: Check whether the URL supports automated checkout 1. Check whether the user has provided a speci…