[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6H26BPRBXZY2YKFN9ZPQFH3":3,"skill-same:01M6H26BPRBXZY2YKFN9ZPQFH3":89},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":40,"linkOnly":41,"markdownContent":42,"markdownBody":43,"frontMatter":44,"refName":52,"commitSha":53,"skillDirectory":54,"forksCount":55,"repositoryHtmlUrl":56,"repositoryHomepageUrl":6,"ownerHtmlUrl":57,"ownerBlogUrl":58,"ownerDescription":59,"updatedAt":60,"install":61,"resources":74},"01M6H26BPRBXZY2YKFN9ZPQFH3","bankr-shopify",null,"Shopify Admin & Storefront GraphQL APIs via curl, with Bankr-native bridges. Manage products, orders, customers, inventory, metafields, webhooks, and bulk ops, then wire merchant data to onchain primitives — store a Bankr-resolvable handle (ENS, Twitter, Farcaster, wallet) on each customer as a metafield, expose Shopify draft orders behind x402 endpoints Bankr settles in USDC, and turn ORDERS_PAID webhooks into Bankr agent jobs for loyalty drops or royalty splits. Triggers: \"Shopify products\", \"Shopify orders\", \"create draft order\", \"loyalty drop on order\", \"x402 checkout\", \"tokengate Shopify\". Core Shopify content adapted with attribution from NousResearch\u002Fhermes-agent (MIT).","jiayaoqijia\u002Fcryptoskill","jiayaoqijia","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1453879?v=4","01M6GMYTX7Q7S504VC6E756ENR","YQ","01M6GMYV1QKJ1CC75MTTGT9PR0",81,"AGPL-3.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Ftrading\u002Fbankrbot-official-bankr-shopify\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"bankr shopify","shopify","bankr","paid","customer","loyalty","commerce","hermes-agent","native","bulk","checkout","farcaster","drop","x402","ecommerce","onchain","Wallet","webhooks","inventory","data","create","2026-11-10 10:12:30",false,"---\nname: bankr-shopify\ndescription: >\n  Shopify Admin & Storefront GraphQL APIs via curl, with Bankr-native bridges.\n  Manage products, orders, customers, inventory, metafields, webhooks, and bulk\n  ops, then wire merchant data to onchain primitives — store a Bankr-resolvable\n  handle (ENS, Twitter, Farcaster, wallet) on each customer as a metafield,\n  expose Shopify draft orders behind x402 endpoints Bankr settles in USDC, and\n  turn ORDERS_PAID webhooks into Bankr agent jobs for loyalty drops or royalty\n  splits. Triggers: \"Shopify products\", \"Shopify orders\", \"create draft order\",\n  \"loyalty drop on order\", \"x402 checkout\", \"tokengate Shopify\". Core Shopify\n  content adapted with attribution from NousResearch\u002Fhermes-agent (MIT).\nemoji: ?️\ntags: [shopify, ecommerce, commerce, graphql, x402, usdc, loyalty, base]\nvisibility: public\n---\n\n# Shopify — Admin & Storefront GraphQL APIs (Bankr Edition)\n\n> The core Shopify sections of this skill are adapted from the upstream [`NousResearch\u002Fhermes-agent`](https:\u002F\u002Fgithub.com\u002FNousResearch\u002Fhermes-agent\u002Fblob\u002Fmain\u002Foptional-skills\u002Fproductivity\u002Fshopify\u002FSKILL.md) skill (MIT, `author: community`). The four \"Bankr Bridges\" sections at the end are new and specific to the Bankr ecosystem.\n\nWork with Shopify stores directly through `curl`: list products, manage inventory, pull orders, update customers, read metafields. No SDK, no app framework — just the GraphQL endpoint and a custom-app access token. Then bridge the merchant data to onchain primitives via Bankr.\n\nThe REST Admin API is legacy since 2024-04 and only receives security fixes. **Use GraphQL Admin** for all admin work. Use **Storefront GraphQL** for read-only customer-facing queries (products, collections, cart).\n\n## Prerequisites\n\n1. In Shopify admin: **Settings → Apps and sales channels → Develop apps → Create an app**.\n2. Click **Configure Admin API scopes**, select what you need (examples below), save.\n3. **Install app** → the Admin API access token appears ONCE. Copy it immediately — Shopify will never show it again. Tokens start with `shpat_`.\n4. Set these in your Bankr settings (gear icon → Env Vars):\n   - `SHOPIFY_ACCESS_TOKEN` — admin token (starts with `shpat_`)\n   - `SHOPIFY_STORE_DOMAIN` — `my-store.myshopify.com` (the permanent myshopify domain, not your custom one)\n   - `SHOPIFY_API_VERSION` — default `2026-01`\n   - `BANKR_API_KEY` — for the Bankr bridge sections; generate at https:\u002F\u002Fbankr.bot\u002Fapi-keys\n\n> **Heads up:** As of January 1, 2026, new \"legacy custom apps\" created in the Shopify admin are gone. New setups should use the **Dev Dashboard** (`shopify.dev\u002Fdocs\u002Fapps\u002Fbuild\u002Fdev-dashboard`). Existing admin-created apps keep working. If the user's shop has no existing custom app and it's after 2026-01-01, direct them to Dev Dashboard instead of the admin flow.\n\nCommon scopes by task:\n- Products \u002F collections: `read_products`, `write_products`\n- Inventory: `read_inventory`, `write_inventory`, `read_locations`\n- Orders: `read_orders`, `write_orders` (30 most recent without `read_all_orders`)\n- Customers: `read_customers`, `write_customers`\n- Draft orders: `read_draft_orders`, `write_draft_orders`\n- Fulfillments: `read_fulfillments`, `write_fulfillments`\n- Metafields \u002F metaobjects: covered by the matching resource scopes\n\n## API Basics\n\n- **Endpoint:** `https:\u002F\u002F$SHOPIFY_STORE_DOMAIN\u002Fadmin\u002Fapi\u002F$SHOPIFY_API_VERSION\u002Fgraphql.json`\n- **Auth header:** `X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN` (NOT `Authorization: Bearer`)\n- **Method:** always `POST`, always `Content-Type: application\u002Fjson`, body is `{\"query\": \"...\", \"variables\": {...}}`\n- **HTTP 200 does not mean success.** GraphQL returns errors in a top-level `errors` array and per-field `userErrors`. Always check both.\n- **IDs are GID strings:** `gid:\u002F\u002Fshopify\u002FProduct\u002F10079467700516`, `gid:\u002F\u002Fshopify\u002FVariant\u002F...`, `gid:\u002F\u002Fshopify\u002FOrder\u002F...`. Pass these verbatim — don't strip the prefix.\n- **Rate limit:** calculated via query cost (leaky bucket). Each response has `extensions.cost` with `requestedQueryCost`, `actualQueryCost`, `throttleStatus.{currentlyAvailable, maximumAvailable, restoreRate}`. Back off when `currentlyAvailable` drops below your next query's cost. Standard shops = 100 points bucket, 50\u002Fs restore; Plus = 1000\u002F100.\n\nBase curl pattern (reusable):\n\n```bash\nshop_gql() {\n  local query=\"$1\"\n  local variables=\"${2:-{}}\"\n  curl -sS -X POST \\\n    \"https:\u002F\u002F${SHOPIFY_STORE_DOMAIN}\u002Fadmin\u002Fapi\u002F${SHOPIFY_API_VERSION:-2026-01}\u002Fgraphql.json\" \\\n    -H \"Content-Type: application\u002Fjson\" \\\n    -H \"X-Shopify-Access-Token: ${SHOPIFY_ACCESS_TOKEN}\" \\\n    --data \"$(jq -nc --arg q \"$query\" --argjson v \"$variables\" '{query: $q, variables: $v}')\"\n}\n```\n\nPipe through `jq` for readable output. `-sS` keeps errors visible but hides the progress bar.\n\n## Discovery\n\n### Shop info + current API version\n```bash\nshop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq\n```\n\n### List all supported API versions\n```bash\nshop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'\n```\n\n## Products\n\n### Search products (first 20 matching query)\n```bash\nshop_gql '\nquery($q: String!) {\n  products(first: 20, query: $q) {\n    edges { node { id title handle status totalInventory variants(first: 5) { edges { node { id sku price inventoryQuantity } } } } }\n    pageInfo { hasNextPage endCursor }\n  }\n}' '{\"q\":\"hoodie status:active\"}' | jq\n```\n\nQuery syntax supports `title:`, `sku:`, `vendor:`, `product_type:`, `status:active`, `tag:`, `created_at:>2025-01-01`. Full grammar: https:\u002F\u002Fshopify.dev\u002Fdocs\u002Fapi\u002Fusage\u002Fsearch-syntax\n\n### Paginate products (cursor)\n```bash\nshop_gql '\nquery($cursor: String) {\n  products(first: 100, after: $cursor) {\n    edges { cursor node { id handle } }\n    pageInfo { hasNextPage endCursor }\n  }\n}' '{\"cursor\":null}'\n# subsequent calls: pass the previous endCursor\n```\n\n### Get a product with variants + metafields\n```bash\nshop_gql '\nquery($id: ID!) {\n  product(id: $id) {\n    id title handle descriptionHtml tags status\n    variants(first: 20) { edges { node { id sku price compareAtPrice inventoryQuantity selectedOptions { name value } } } }\n    metafields(first: 20) { edges { node { namespace key type value } } }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F10079467700516\"}' | jq\n```\n\n### Create a product with one variant\n```bash\nshop_gql '\nmutation($input: ProductCreateInput!) {\n  productCreate(product: $input) {\n    product { id handle }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"title\":\"Test Hoodie\",\"status\":\"DRAFT\",\"vendor\":\"Bankr\",\"productType\":\"Apparel\",\"tags\":[\"test\"]}}'\n```\n\nVariants now have their own mutations in recent versions:\n\n```bash\n# Add variants after creating the product\nshop_gql '\nmutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {\n  productVariantsBulkCreate(productId: $productId, variants: $variants) {\n    productVariants { id sku price }\n    userErrors { field message }\n  }\n}' '{\"productId\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\",\"variants\":[{\"optionValues\":[{\"optionName\":\"Size\",\"name\":\"M\"}],\"price\":\"49.00\",\"inventoryItem\":{\"sku\":\"HD-M\",\"tracked\":true}}]}'\n```\n\n### Update price \u002F SKU\n```bash\nshop_gql '\nmutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {\n  productVariantsBulkUpdate(productId: $productId, variants: $variants) {\n    productVariants { id sku price }\n    userErrors { field message }\n  }\n}' '{\"productId\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\",\"variants\":[{\"id\":\"gid:\u002F\u002Fshopify\u002FProductVariant\u002F...\",\"price\":\"55.00\"}]}'\n```\n\n## Orders\n\n### List recent orders (last 30 by default without `read_all_orders`)\n```bash\nshop_gql '\n{\n  orders(first: 20, reverse: true, query: \"financial_status:paid\") {\n    edges { node {\n      id name createdAt displayFinancialStatus displayFulfillmentStatus\n      totalPriceSet { shopMoney { amount currencyCode } }\n      customer { id displayName email }\n      lineItems(first: 10) { edges { node { title quantity sku } } }\n    } }\n  }\n}' | jq\n```\n\nUseful order query filters: `financial_status:paid|pending|refunded`, `fulfillment_status:unfulfilled|fulfilled`, `created_at:>2025-01-01`, `tag:gift`, `email:foo@example.com`.\n\n### Fetch a single order with shipping address\n```bash\nshop_gql '\nquery($id: ID!) {\n  order(id: $id) {\n    id name email\n    shippingAddress { name address1 address2 city province country zip phone }\n    lineItems(first: 50) { edges { node { title quantity variant { sku } originalUnitPriceSet { shopMoney { amount currencyCode } } } } }\n    transactions { id kind status amountSet { shopMoney { amount currencyCode } } }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FOrder\u002F....\"}' | jq\n```\n\n## Customers\n\n```bash\n# Search\nshop_gql '\n{\n  customers(first: 10, query: \"email:*@example.com\") {\n    edges { node { id email displayName numberOfOrders amountSpent { amount currencyCode } } }\n  }\n}'\n\n# Create\nshop_gql '\nmutation($input: CustomerInput!) {\n  customerCreate(input: $input) {\n    customer { id email }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"email\":\"test@example.com\",\"firstName\":\"Test\",\"lastName\":\"User\",\"tags\":[\"api-created\"]}}'\n```\n\n## Inventory\n\nInventory lives on **inventory items** tied to variants, quantities tracked per **location**.\n\n```bash\n# Get inventory for a variant across all locations\nshop_gql '\nquery($id: ID!) {\n  productVariant(id: $id) {\n    id sku\n    inventoryItem {\n      id tracked\n      inventoryLevels(first: 10) {\n        edges { node { location { id name } quantities(names: [\"available\",\"on_hand\",\"committed\"]) { name quantity } } }\n      }\n    }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FProductVariant\u002F...\"}'\n```\n\nAdjust stock (delta) — uses `inventoryAdjustQuantities`:\n\n```bash\nshop_gql '\nmutation($input: InventoryAdjustQuantitiesInput!) {\n  inventoryAdjustQuantities(input: $input) {\n    inventoryAdjustmentGroup { reason changes { name delta } }\n    userErrors { field message }\n  }\n}' '{\n  \"input\": {\n    \"reason\": \"correction\",\n    \"name\": \"available\",\n    \"changes\": [{\"delta\": 5, \"inventoryItemId\": \"gid:\u002F\u002Fshopify\u002FInventoryItem\u002F...\", \"locationId\": \"gid:\u002F\u002Fshopify\u002FLocation\u002F...\"}]\n  }\n}'\n```\n\nSet absolute stock (not delta) — `inventorySetQuantities`:\n\n```bash\nshop_gql '\nmutation($input: InventorySetQuantitiesInput!) {\n  inventorySetQuantities(input: $input) {\n    inventoryAdjustmentGroup { id }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"reason\":\"correction\",\"name\":\"available\",\"ignoreCompareQuantity\":true,\"quantities\":[{\"inventoryItemId\":\"gid:\u002F\u002Fshopify\u002FInventoryItem\u002F...\",\"locationId\":\"gid:\u002F\u002Fshopify\u002FLocation\u002F...\",\"quantity\":100}]}}'\n```\n\n## Metafields & Metaobjects\n\nMetafields attach custom data to resources (products, customers, orders, shop).\n\n```bash\n# Read\nshop_gql '\nquery($id: ID!) {\n  product(id: $id) {\n    metafields(first: 10, namespace: \"custom\") {\n      edges { node { key type value } }\n    }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\"}'\n\n# Write (works for any owner type)\nshop_gql '\nmutation($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields { id key namespace }\n    userErrors { field message code }\n  }\n}' '{\"metafields\":[{\"ownerId\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\",\"namespace\":\"custom\",\"key\":\"care_instructions\",\"type\":\"multi_line_text_field\",\"value\":\"Wash cold. Tumble dry low.\"}]}'\n```\n\n## Storefront API (public read-only)\n\nDifferent endpoint, different token, used for customer-facing apps\u002Fhydrogen-style headless setups. Headers differ:\n\n- **Endpoint:** `https:\u002F\u002F$SHOPIFY_STORE_DOMAIN\u002Fapi\u002F$SHOPIFY_API_VERSION\u002Fgraphql.json`\n- **Auth header (public):** `X-Shopify-Storefront-Access-Token: \u003Cpublic token>` — embeddable in browser\n- **Auth header (private):** `Shopify-Storefront-Private-Token: \u003Cprivate token>` — server-only\n\n```bash\ncurl -sS -X POST \\\n  \"https:\u002F\u002F${SHOPIFY_STORE_DOMAIN}\u002Fapi\u002F${SHOPIFY_API_VERSION:-2026-01}\u002Fgraphql.json\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Shopify-Storefront-Access-Token: ${SHOPIFY_STOREFRONT_TOKEN}\" \\\n  -d '{\"query\":\"{ shop { name } products(first: 5) { edges { node { id title handle } } } }\"}' | jq\n```\n\n## Bulk Operations\n\nFor dumps larger than rate limits allow (full product catalog, all orders for a year):\n\n```bash\n# 1. Start bulk query\nshop_gql '\nmutation {\n  bulkOperationRunQuery(query: \"\"\"\n    { products { edges { node { id title handle variants { edges { node { sku price } } } } } } }\n  \"\"\") {\n    bulkOperation { id status }\n    userErrors { field message }\n  }\n}'\n\n# 2. Poll status\nshop_gql '{ currentBulkOperation { id status errorCode objectCount fileSize url partialDataUrl } }'\n\n# 3. When status=COMPLETED, download the JSONL file\ncurl -sS \"$URL\" > products.jsonl\n```\n\nEach JSONL line is a node, and nested connections are emitted as separate lines with `__parentId`. Reassemble client-side if needed.\n\n## Webhooks\n\nSubscribe to events so you don't have to poll:\n\n```bash\nshop_gql '\nmutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) {\n  webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) {\n    webhookSubscription { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } }\n    userErrors { field message }\n  }\n}' '{\"topic\":\"ORDERS_CREATE\",\"sub\":{\"callbackUrl\":\"https:\u002F\u002Fexample.com\u002Fwebhook\",\"format\":\"JSON\"}}'\n```\n\nVerify incoming webhook HMAC using the app's client secret (not the access token):\n\n```bash\necho -n \"$REQUEST_BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" -binary | base64\n# Compare to X-Shopify-Hmac-Sha256 header\n```\n\n## Pitfalls\n\n- **REST endpoints still exist but are frozen.** Don't write new integrations against `\u002Fadmin\u002Fapi\u002F...\u002Fproducts.json`. Use GraphQL.\n- **Token format check.** Admin tokens start with `shpat_`. Storefront public tokens with `shpua_`. If you have one and the wrong header, every request returns 401 without a useful error body.\n- **403 with a valid token = missing scope.** Shopify returns `{\"errors\":[{\"message\":\"Access denied for ...\"}]}`. Re-configure Admin API scopes on the app, then reinstall to regenerate the token.\n- **`userErrors` is empty != success.** Also check `data.\u003Cmutation>.\u003Cresource>` is non-null. Some failures populate neither — inspect the whole response.\n- **GID vs numeric ID.** Legacy REST gave numeric IDs; GraphQL wants full GID strings. To convert: `gid:\u002F\u002Fshopify\u002FProduct\u002F\u003Cnumeric>`.\n- **Rate limit surprise.** A single `products(first: 250)` with deep nesting can cost 1000+ points and throttle immediately on a standard-plan shop. Start narrow, read `extensions.cost`, adjust.\n- **Pagination order.** `products(first: N, reverse: true)` sorts by `id DESC`, not `created_at`. Use `sortKey: CREATED_AT, reverse: true` for \"newest first.\"\n- **`read_all_orders` for historical data.** Without it, `orders(...)` silently caps at the 60-day window. You won't get an error, just fewer results than expected. For Shopify Plus merchants with many orders, request this scope via the app's protected-data settings.\n- **Currencies are strings.** Amounts come back as `\"49.00\"` not `49.0`. Don't `jq tonumber` blindly if you care about zero-padding.\n- **Multi-currency Money fields** have `shopMoney` (store's currency) AND `presentmentMoney` (customer's). Pick one consistently.\n\n## Safety\n\nMutations in Shopify are real — they create products, charge refunds, cancel orders, ship fulfillments. Before running `productDelete`, `orderCancel`, `refundCreate`, or any bulk mutation: state clearly what the change is, on which shop, and confirm with the user. There is no staging clone of production data unless the user has a separate dev store.\n\n---\n\n# Bankr Bridges\n\nThe sections below are not part of the upstream Hermes skill. They show how to wire Shopify resources to Bankr's onchain primitives — handle resolution, x402 settlement, and webhook-driven token flows — using only what Bankr already exposes.\n\nPer the [Bankr docs](https:\u002F\u002Fdocs.bankr.bot\u002Fllms-full.txt):\n- Bankr's agent natively resolves **ENS, Twitter @handle, Farcaster handle, and wallet addresses** for transfers and fee recipients. Email is **not** resolvable.\n- Bankr can **call, host, and settle x402 endpoints**; USDC is the unit.\n- Job pattern is **submit → poll**: `POST \u002Fagent\u002Fprompt` then `GET \u002Fagent\u002Fjob\u002F{id}`. There are no Bankr-side webhooks.\n- MCP tools: `bankr_agent_submit_prompt`, `bankr_agent_get_job_status`, `bankr_agent_cancel_job`.\n\n## Bridge 1 — Identity (Shopify customer ↔ Bankr handle)\n\nShopify's primary customer key is **email**, which Bankr cannot resolve. The bridge: store a Bankr-resolvable handle on the customer as a metafield, then pass the string verbatim to Bankr.\n\nRecommended metafield definition: `namespace: custom`, `key: handle`, `type: single_line_text_field`. Acceptable values: `vitalik.eth`, `@dwr.eth`, `@username` (Twitter), or a `0x…` wallet address.\n\n```bash\n# Write a handle onto a customer at checkout \u002F signup\nshop_gql '\nmutation($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields { id key value }\n    userErrors { field message code }\n  }\n}' '{\"metafields\":[{\"ownerId\":\"gid:\u002F\u002Fshopify\u002FCustomer\u002F123\",\"namespace\":\"custom\",\"key\":\"handle\",\"type\":\"single_line_text_field\",\"value\":\"vitalik.eth\"}]}'\n```\n\nRead it back when you need to pay or drop tokens:\n\n```bash\nHANDLE=$(shop_gql '\nquery($id: ID!) {\n  customer(id: $id) { metafield(namespace:\"custom\", key:\"handle\") { value } }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FCustomer\u002F123\"}' | jq -r '.data.customer.metafield.value')\n\n# Hand off to Bankr — no extra resolution needed.\ncurl -sS -X POST https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fprompt \\\n  -H \"Authorization: Bearer ${BANKR_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"prompt\\\":\\\"send 10 USDC to ${HANDLE} on base\\\"}\"\n```\n\nIf the metafield is empty, prompt the customer for one of {ENS, Twitter, Farcaster, wallet} before any onchain action — never guess from email.\n\n## Bridge 2 — x402 checkout for a Shopify draft order\n\nBankr settles x402 endpoints natively. To accept USDC for a Shopify cart, mint a draft order, expose its total behind an x402-priced endpoint, and let Bankr pay it. On 200, mark the draft order as paid (or call `draftOrderComplete`).\n\n```bash\n# 1. Create a draft order from a cart\nDRAFT=$(shop_gql '\nmutation($input: DraftOrderInput!) {\n  draftOrderCreate(input: $input) {\n    draftOrder { id totalPriceSet { shopMoney { amount currencyCode } } invoiceUrl }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"lineItems\":[{\"variantId\":\"gid:\u002F\u002Fshopify\u002FProductVariant\u002F...\",\"quantity\":1}],\"email\":\"buyer@example.com\"}}')\n\nDRAFT_ID=$(echo \"$DRAFT\" | jq -r '.data.draftOrderCreate.draftOrder.id')\nTOTAL=$(echo \"$DRAFT\" | jq -r '.data.draftOrderCreate.draftOrder.totalPriceSet.shopMoney.amount')\n```\n\nYour service then exposes an HTTP 402 endpoint pricing this draft (USDC on Base) — see the upstream x402 spec. Bankr-side, the agent settles it:\n\n```bash\n# Bankr-side (agent): pay the x402 endpoint that fronts the draft order\ncurl -sS -X POST https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fprompt \\\n  -H \"Authorization: Bearer ${BANKR_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"prompt\\\":\\\"call x402 endpoint https:\u002F\u002Fshop.example.com\u002Fx402\u002Fdraft\u002F${DRAFT_ID} and settle in USDC on base\\\"}\"\n```\n\nWhen your x402 server confirms settlement, complete the draft on the Shopify side:\n\n```bash\nshop_gql '\nmutation($id: ID!) {\n  draftOrderComplete(id: $id, paymentPending: false) {\n    draftOrder { order { id name } }\n    userErrors { field message }\n  }\n}' \"{\\\"id\\\":\\\"${DRAFT_ID}\\\"}\"\n```\n\nNotes:\n- Use **`shopMoney`** consistently for x402 pricing so currency conversion stays on your side, not Shopify's.\n- Treat the x402 settlement tx hash as the source of truth — do not complete the draft until you've verified the hash.\n\n## Bridge 3 — Shopify webhook → Bankr Submit (loyalty drop on `ORDERS_PAID`)\n\nBankr has no inbound webhooks. The pattern is: Shopify webhook → your server → HMAC verify → read customer handle metafield → submit a Bankr job → poll.\n\n```bash\n# Subscribe once\nshop_gql '\nmutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) {\n  webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) {\n    webhookSubscription { id topic }\n    userErrors { field message }\n  }\n}' '{\"topic\":\"ORDERS_PAID\",\"sub\":{\"callbackUrl\":\"https:\u002F\u002Fyour.server\u002Fshopify\u002Forders-paid\",\"format\":\"JSON\"}}'\n```\n\nWebhook handler pseudocode:\n\n```bash\n# 1. Verify HMAC (reject if it doesn't match) — use a constant-time comparison in real code\nEXPECTED=$(echo -n \"$REQUEST_BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" -binary | base64)\n[ \"$EXPECTED\" = \"$X_SHOPIFY_HMAC_SHA256\" ] || exit 1\n\n# 2. Pull the buyer's Bankr handle from the order's customer metafield\nCUSTOMER_ID=$(echo \"$REQUEST_BODY\" | jq -r '.customer.admin_graphql_api_id')\nHANDLE=$(shop_gql '\nquery($id: ID!) {\n  customer(id: $id) { metafield(namespace:\"custom\", key:\"handle\") { value } }\n}' \"{\\\"id\\\":\\\"${CUSTOMER_ID}\\\"}\" | jq -r '.data.customer.metafield.value')\n[ -z \"$HANDLE\" ] || [ \"$HANDLE\" = \"null\" ] && exit 0   # silently skip, no handle on file\n\n# 3. Submit the loyalty drop to Bankr\nJOB=$(curl -sS -X POST https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fprompt \\\n  -H \"Authorization: Bearer ${BANKR_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"prompt\\\":\\\"send 100 LOYALTY to ${HANDLE} on base\\\"}\" | jq -r '.job_id')\n\n# 4. Poll until terminal\nwhile :; do\n  STATUS=$(curl -sS \"https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fjob\u002F${JOB}\" \\\n    -H \"Authorization: Bearer ${BANKR_API_KEY}\" | jq -r '.status')\n  case \"$STATUS\" in\n    completed|failed|cancelled) break ;;\n  esac\n  sleep 2\ndone\n```\n\nSame shape works for: royalty splits on digital goods, Clanker token airdrops to repeat buyers, treasury sweeps when a sales threshold is hit. Add an idempotency key on the Shopify order id so a replayed webhook cannot trigger a duplicate Bankr submission.\n\n## Bridge 4 — Scope cheatsheet for the Bankr flows\n\nMinimum Admin API scopes per bridge:\n\n| Flow | Required scopes |\n| --- | --- |\n| Identity bridge (read\u002Fwrite handle metafield) | `read_customers`, `write_customers` |\n| x402 checkout via draft orders | `read_customers`, `write_draft_orders`, `read_orders` |\n| Webhook → Bankr loyalty drop | `read_orders`, `read_customers`, plus webhook subscription on `ORDERS_PAID` |\n| Bulk export for an offline reconciliation against onchain tx hashes | `read_products`, `read_orders`, `read_customers` |\n\nToken only needs what the agent will actually use. Don't grant `write_*` scopes to a read-only reconciliation agent.\n\n---\n\n## Credit\n\nCore Shopify content (everything above the \"Bankr Bridges\" header) is adapted, with attribution, from `NousResearch\u002Fhermes-agent` (`optional-skills\u002Fproductivity\u002Fshopify\u002FSKILL.md`), MIT licensed. The Bankr bridge sections are new and contributed under the same MIT license.\n","# Shopify — Admin & Storefront GraphQL APIs (Bankr Edition)\n\n> The core Shopify sections of this skill are adapted from the upstream [`NousResearch\u002Fhermes-agent`](https:\u002F\u002Fgithub.com\u002FNousResearch\u002Fhermes-agent\u002Fblob\u002Fmain\u002Foptional-skills\u002Fproductivity\u002Fshopify\u002FSKILL.md) skill (MIT, `author: community`). The four \"Bankr Bridges\" sections at the end are new and specific to the Bankr ecosystem.\n\nWork with Shopify stores directly through `curl`: list products, manage inventory, pull orders, update customers, read metafields. No SDK, no app framework — just the GraphQL endpoint and a custom-app access token. Then bridge the merchant data to onchain primitives via Bankr.\n\nThe REST Admin API is legacy since 2024-04 and only receives security fixes. **Use GraphQL Admin** for all admin work. Use **Storefront GraphQL** for read-only customer-facing queries (products, collections, cart).\n\n## Prerequisites\n\n1. In Shopify admin: **Settings → Apps and sales channels → Develop apps → Create an app**.\n2. Click **Configure Admin API scopes**, select what you need (examples below), save.\n3. **Install app** → the Admin API access token appears ONCE. Copy it immediately — Shopify will never show it again. Tokens start with `shpat_`.\n4. Set these in your Bankr settings (gear icon → Env Vars):\n   - `SHOPIFY_ACCESS_TOKEN` — admin token (starts with `shpat_`)\n   - `SHOPIFY_STORE_DOMAIN` — `my-store.myshopify.com` (the permanent myshopify domain, not your custom one)\n   - `SHOPIFY_API_VERSION` — default `2026-01`\n   - `BANKR_API_KEY` — for the Bankr bridge sections; generate at https:\u002F\u002Fbankr.bot\u002Fapi-keys\n\n> **Heads up:** As of January 1, 2026, new \"legacy custom apps\" created in the Shopify admin are gone. New setups should use the **Dev Dashboard** (`shopify.dev\u002Fdocs\u002Fapps\u002Fbuild\u002Fdev-dashboard`). Existing admin-created apps keep working. If the user's shop has no existing custom app and it's after 2026-01-01, direct them to Dev Dashboard instead of the admin flow.\n\nCommon scopes by task:\n- Products \u002F collections: `read_products`, `write_products`\n- Inventory: `read_inventory`, `write_inventory`, `read_locations`\n- Orders: `read_orders`, `write_orders` (30 most recent without `read_all_orders`)\n- Customers: `read_customers`, `write_customers`\n- Draft orders: `read_draft_orders`, `write_draft_orders`\n- Fulfillments: `read_fulfillments`, `write_fulfillments`\n- Metafields \u002F metaobjects: covered by the matching resource scopes\n\n## API Basics\n\n- **Endpoint:** `https:\u002F\u002F$SHOPIFY_STORE_DOMAIN\u002Fadmin\u002Fapi\u002F$SHOPIFY_API_VERSION\u002Fgraphql.json`\n- **Auth header:** `X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN` (NOT `Authorization: Bearer`)\n- **Method:** always `POST`, always `Content-Type: application\u002Fjson`, body is `{\"query\": \"...\", \"variables\": {...}}`\n- **HTTP 200 does not mean success.** GraphQL returns errors in a top-level `errors` array and per-field `userErrors`. Always check both.\n- **IDs are GID strings:** `gid:\u002F\u002Fshopify\u002FProduct\u002F10079467700516`, `gid:\u002F\u002Fshopify\u002FVariant\u002F...`, `gid:\u002F\u002Fshopify\u002FOrder\u002F...`. Pass these verbatim — don't strip the prefix.\n- **Rate limit:** calculated via query cost (leaky bucket). Each response has `extensions.cost` with `requestedQueryCost`, `actualQueryCost`, `throttleStatus.{currentlyAvailable, maximumAvailable, restoreRate}`. Back off when `currentlyAvailable` drops below your next query's cost. Standard shops = 100 points bucket, 50\u002Fs restore; Plus = 1000\u002F100.\n\nBase curl pattern (reusable):\n\n```bash\nshop_gql() {\n  local query=\"$1\"\n  local variables=\"${2:-{}}\"\n  curl -sS -X POST \\\n    \"https:\u002F\u002F${SHOPIFY_STORE_DOMAIN}\u002Fadmin\u002Fapi\u002F${SHOPIFY_API_VERSION:-2026-01}\u002Fgraphql.json\" \\\n    -H \"Content-Type: application\u002Fjson\" \\\n    -H \"X-Shopify-Access-Token: ${SHOPIFY_ACCESS_TOKEN}\" \\\n    --data \"$(jq -nc --arg q \"$query\" --argjson v \"$variables\" '{query: $q, variables: $v}')\"\n}\n```\n\nPipe through `jq` for readable output. `-sS` keeps errors visible but hides the progress bar.\n\n## Discovery\n\n### Shop info + current API version\n```bash\nshop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq\n```\n\n### List all supported API versions\n```bash\nshop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'\n```\n\n## Products\n\n### Search products (first 20 matching query)\n```bash\nshop_gql '\nquery($q: String!) {\n  products(first: 20, query: $q) {\n    edges { node { id title handle status totalInventory variants(first: 5) { edges { node { id sku price inventoryQuantity } } } } }\n    pageInfo { hasNextPage endCursor }\n  }\n}' '{\"q\":\"hoodie status:active\"}' | jq\n```\n\nQuery syntax supports `title:`, `sku:`, `vendor:`, `product_type:`, `status:active`, `tag:`, `created_at:>2025-01-01`. Full grammar: https:\u002F\u002Fshopify.dev\u002Fdocs\u002Fapi\u002Fusage\u002Fsearch-syntax\n\n### Paginate products (cursor)\n```bash\nshop_gql '\nquery($cursor: String) {\n  products(first: 100, after: $cursor) {\n    edges { cursor node { id handle } }\n    pageInfo { hasNextPage endCursor }\n  }\n}' '{\"cursor\":null}'\n# subsequent calls: pass the previous endCursor\n```\n\n### Get a product with variants + metafields\n```bash\nshop_gql '\nquery($id: ID!) {\n  product(id: $id) {\n    id title handle descriptionHtml tags status\n    variants(first: 20) { edges { node { id sku price compareAtPrice inventoryQuantity selectedOptions { name value } } } }\n    metafields(first: 20) { edges { node { namespace key type value } } }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F10079467700516\"}' | jq\n```\n\n### Create a product with one variant\n```bash\nshop_gql '\nmutation($input: ProductCreateInput!) {\n  productCreate(product: $input) {\n    product { id handle }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"title\":\"Test Hoodie\",\"status\":\"DRAFT\",\"vendor\":\"Bankr\",\"productType\":\"Apparel\",\"tags\":[\"test\"]}}'\n```\n\nVariants now have their own mutations in recent versions:\n\n```bash\n# Add variants after creating the product\nshop_gql '\nmutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {\n  productVariantsBulkCreate(productId: $productId, variants: $variants) {\n    productVariants { id sku price }\n    userErrors { field message }\n  }\n}' '{\"productId\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\",\"variants\":[{\"optionValues\":[{\"optionName\":\"Size\",\"name\":\"M\"}],\"price\":\"49.00\",\"inventoryItem\":{\"sku\":\"HD-M\",\"tracked\":true}}]}'\n```\n\n### Update price \u002F SKU\n```bash\nshop_gql '\nmutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {\n  productVariantsBulkUpdate(productId: $productId, variants: $variants) {\n    productVariants { id sku price }\n    userErrors { field message }\n  }\n}' '{\"productId\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\",\"variants\":[{\"id\":\"gid:\u002F\u002Fshopify\u002FProductVariant\u002F...\",\"price\":\"55.00\"}]}'\n```\n\n## Orders\n\n### List recent orders (last 30 by default without `read_all_orders`)\n```bash\nshop_gql '\n{\n  orders(first: 20, reverse: true, query: \"financial_status:paid\") {\n    edges { node {\n      id name createdAt displayFinancialStatus displayFulfillmentStatus\n      totalPriceSet { shopMoney { amount currencyCode } }\n      customer { id displayName email }\n      lineItems(first: 10) { edges { node { title quantity sku } } }\n    } }\n  }\n}' | jq\n```\n\nUseful order query filters: `financial_status:paid|pending|refunded`, `fulfillment_status:unfulfilled|fulfilled`, `created_at:>2025-01-01`, `tag:gift`, `email:foo@example.com`.\n\n### Fetch a single order with shipping address\n```bash\nshop_gql '\nquery($id: ID!) {\n  order(id: $id) {\n    id name email\n    shippingAddress { name address1 address2 city province country zip phone }\n    lineItems(first: 50) { edges { node { title quantity variant { sku } originalUnitPriceSet { shopMoney { amount currencyCode } } } } }\n    transactions { id kind status amountSet { shopMoney { amount currencyCode } } }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FOrder\u002F....\"}' | jq\n```\n\n## Customers\n\n```bash\n# Search\nshop_gql '\n{\n  customers(first: 10, query: \"email:*@example.com\") {\n    edges { node { id email displayName numberOfOrders amountSpent { amount currencyCode } } }\n  }\n}'\n\n# Create\nshop_gql '\nmutation($input: CustomerInput!) {\n  customerCreate(input: $input) {\n    customer { id email }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"email\":\"test@example.com\",\"firstName\":\"Test\",\"lastName\":\"User\",\"tags\":[\"api-created\"]}}'\n```\n\n## Inventory\n\nInventory lives on **inventory items** tied to variants, quantities tracked per **location**.\n\n```bash\n# Get inventory for a variant across all locations\nshop_gql '\nquery($id: ID!) {\n  productVariant(id: $id) {\n    id sku\n    inventoryItem {\n      id tracked\n      inventoryLevels(first: 10) {\n        edges { node { location { id name } quantities(names: [\"available\",\"on_hand\",\"committed\"]) { name quantity } } }\n      }\n    }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FProductVariant\u002F...\"}'\n```\n\nAdjust stock (delta) — uses `inventoryAdjustQuantities`:\n\n```bash\nshop_gql '\nmutation($input: InventoryAdjustQuantitiesInput!) {\n  inventoryAdjustQuantities(input: $input) {\n    inventoryAdjustmentGroup { reason changes { name delta } }\n    userErrors { field message }\n  }\n}' '{\n  \"input\": {\n    \"reason\": \"correction\",\n    \"name\": \"available\",\n    \"changes\": [{\"delta\": 5, \"inventoryItemId\": \"gid:\u002F\u002Fshopify\u002FInventoryItem\u002F...\", \"locationId\": \"gid:\u002F\u002Fshopify\u002FLocation\u002F...\"}]\n  }\n}'\n```\n\nSet absolute stock (not delta) — `inventorySetQuantities`:\n\n```bash\nshop_gql '\nmutation($input: InventorySetQuantitiesInput!) {\n  inventorySetQuantities(input: $input) {\n    inventoryAdjustmentGroup { id }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"reason\":\"correction\",\"name\":\"available\",\"ignoreCompareQuantity\":true,\"quantities\":[{\"inventoryItemId\":\"gid:\u002F\u002Fshopify\u002FInventoryItem\u002F...\",\"locationId\":\"gid:\u002F\u002Fshopify\u002FLocation\u002F...\",\"quantity\":100}]}}'\n```\n\n## Metafields & Metaobjects\n\nMetafields attach custom data to resources (products, customers, orders, shop).\n\n```bash\n# Read\nshop_gql '\nquery($id: ID!) {\n  product(id: $id) {\n    metafields(first: 10, namespace: \"custom\") {\n      edges { node { key type value } }\n    }\n  }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\"}'\n\n# Write (works for any owner type)\nshop_gql '\nmutation($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields { id key namespace }\n    userErrors { field message code }\n  }\n}' '{\"metafields\":[{\"ownerId\":\"gid:\u002F\u002Fshopify\u002FProduct\u002F...\",\"namespace\":\"custom\",\"key\":\"care_instructions\",\"type\":\"multi_line_text_field\",\"value\":\"Wash cold. Tumble dry low.\"}]}'\n```\n\n## Storefront API (public read-only)\n\nDifferent endpoint, different token, used for customer-facing apps\u002Fhydrogen-style headless setups. Headers differ:\n\n- **Endpoint:** `https:\u002F\u002F$SHOPIFY_STORE_DOMAIN\u002Fapi\u002F$SHOPIFY_API_VERSION\u002Fgraphql.json`\n- **Auth header (public):** `X-Shopify-Storefront-Access-Token: \u003Cpublic token>` — embeddable in browser\n- **Auth header (private):** `Shopify-Storefront-Private-Token: \u003Cprivate token>` — server-only\n\n```bash\ncurl -sS -X POST \\\n  \"https:\u002F\u002F${SHOPIFY_STORE_DOMAIN}\u002Fapi\u002F${SHOPIFY_API_VERSION:-2026-01}\u002Fgraphql.json\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Shopify-Storefront-Access-Token: ${SHOPIFY_STOREFRONT_TOKEN}\" \\\n  -d '{\"query\":\"{ shop { name } products(first: 5) { edges { node { id title handle } } } }\"}' | jq\n```\n\n## Bulk Operations\n\nFor dumps larger than rate limits allow (full product catalog, all orders for a year):\n\n```bash\n# 1. Start bulk query\nshop_gql '\nmutation {\n  bulkOperationRunQuery(query: \"\"\"\n    { products { edges { node { id title handle variants { edges { node { sku price } } } } } } }\n  \"\"\") {\n    bulkOperation { id status }\n    userErrors { field message }\n  }\n}'\n\n# 2. Poll status\nshop_gql '{ currentBulkOperation { id status errorCode objectCount fileSize url partialDataUrl } }'\n\n# 3. When status=COMPLETED, download the JSONL file\ncurl -sS \"$URL\" > products.jsonl\n```\n\nEach JSONL line is a node, and nested connections are emitted as separate lines with `__parentId`. Reassemble client-side if needed.\n\n## Webhooks\n\nSubscribe to events so you don't have to poll:\n\n```bash\nshop_gql '\nmutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) {\n  webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) {\n    webhookSubscription { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } }\n    userErrors { field message }\n  }\n}' '{\"topic\":\"ORDERS_CREATE\",\"sub\":{\"callbackUrl\":\"https:\u002F\u002Fexample.com\u002Fwebhook\",\"format\":\"JSON\"}}'\n```\n\nVerify incoming webhook HMAC using the app's client secret (not the access token):\n\n```bash\necho -n \"$REQUEST_BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" -binary | base64\n# Compare to X-Shopify-Hmac-Sha256 header\n```\n\n## Pitfalls\n\n- **REST endpoints still exist but are frozen.** Don't write new integrations against `\u002Fadmin\u002Fapi\u002F...\u002Fproducts.json`. Use GraphQL.\n- **Token format check.** Admin tokens start with `shpat_`. Storefront public tokens with `shpua_`. If you have one and the wrong header, every request returns 401 without a useful error body.\n- **403 with a valid token = missing scope.** Shopify returns `{\"errors\":[{\"message\":\"Access denied for ...\"}]}`. Re-configure Admin API scopes on the app, then reinstall to regenerate the token.\n- **`userErrors` is empty != success.** Also check `data.\u003Cmutation>.\u003Cresource>` is non-null. Some failures populate neither — inspect the whole response.\n- **GID vs numeric ID.** Legacy REST gave numeric IDs; GraphQL wants full GID strings. To convert: `gid:\u002F\u002Fshopify\u002FProduct\u002F\u003Cnumeric>`.\n- **Rate limit surprise.** A single `products(first: 250)` with deep nesting can cost 1000+ points and throttle immediately on a standard-plan shop. Start narrow, read `extensions.cost`, adjust.\n- **Pagination order.** `products(first: N, reverse: true)` sorts by `id DESC`, not `created_at`. Use `sortKey: CREATED_AT, reverse: true` for \"newest first.\"\n- **`read_all_orders` for historical data.** Without it, `orders(...)` silently caps at the 60-day window. You won't get an error, just fewer results than expected. For Shopify Plus merchants with many orders, request this scope via the app's protected-data settings.\n- **Currencies are strings.** Amounts come back as `\"49.00\"` not `49.0`. Don't `jq tonumber` blindly if you care about zero-padding.\n- **Multi-currency Money fields** have `shopMoney` (store's currency) AND `presentmentMoney` (customer's). Pick one consistently.\n\n## Safety\n\nMutations in Shopify are real — they create products, charge refunds, cancel orders, ship fulfillments. Before running `productDelete`, `orderCancel`, `refundCreate`, or any bulk mutation: state clearly what the change is, on which shop, and confirm with the user. There is no staging clone of production data unless the user has a separate dev store.\n\n---\n\n# Bankr Bridges\n\nThe sections below are not part of the upstream Hermes skill. They show how to wire Shopify resources to Bankr's onchain primitives — handle resolution, x402 settlement, and webhook-driven token flows — using only what Bankr already exposes.\n\nPer the [Bankr docs](https:\u002F\u002Fdocs.bankr.bot\u002Fllms-full.txt):\n- Bankr's agent natively resolves **ENS, Twitter @handle, Farcaster handle, and wallet addresses** for transfers and fee recipients. Email is **not** resolvable.\n- Bankr can **call, host, and settle x402 endpoints**; USDC is the unit.\n- Job pattern is **submit → poll**: `POST \u002Fagent\u002Fprompt` then `GET \u002Fagent\u002Fjob\u002F{id}`. There are no Bankr-side webhooks.\n- MCP tools: `bankr_agent_submit_prompt`, `bankr_agent_get_job_status`, `bankr_agent_cancel_job`.\n\n## Bridge 1 — Identity (Shopify customer ↔ Bankr handle)\n\nShopify's primary customer key is **email**, which Bankr cannot resolve. The bridge: store a Bankr-resolvable handle on the customer as a metafield, then pass the string verbatim to Bankr.\n\nRecommended metafield definition: `namespace: custom`, `key: handle`, `type: single_line_text_field`. Acceptable values: `vitalik.eth`, `@dwr.eth`, `@username` (Twitter), or a `0x…` wallet address.\n\n```bash\n# Write a handle onto a customer at checkout \u002F signup\nshop_gql '\nmutation($metafields: [MetafieldsSetInput!]!) {\n  metafieldsSet(metafields: $metafields) {\n    metafields { id key value }\n    userErrors { field message code }\n  }\n}' '{\"metafields\":[{\"ownerId\":\"gid:\u002F\u002Fshopify\u002FCustomer\u002F123\",\"namespace\":\"custom\",\"key\":\"handle\",\"type\":\"single_line_text_field\",\"value\":\"vitalik.eth\"}]}'\n```\n\nRead it back when you need to pay or drop tokens:\n\n```bash\nHANDLE=$(shop_gql '\nquery($id: ID!) {\n  customer(id: $id) { metafield(namespace:\"custom\", key:\"handle\") { value } }\n}' '{\"id\":\"gid:\u002F\u002Fshopify\u002FCustomer\u002F123\"}' | jq -r '.data.customer.metafield.value')\n\n# Hand off to Bankr — no extra resolution needed.\ncurl -sS -X POST https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fprompt \\\n  -H \"Authorization: Bearer ${BANKR_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"prompt\\\":\\\"send 10 USDC to ${HANDLE} on base\\\"}\"\n```\n\nIf the metafield is empty, prompt the customer for one of {ENS, Twitter, Farcaster, wallet} before any onchain action — never guess from email.\n\n## Bridge 2 — x402 checkout for a Shopify draft order\n\nBankr settles x402 endpoints natively. To accept USDC for a Shopify cart, mint a draft order, expose its total behind an x402-priced endpoint, and let Bankr pay it. On 200, mark the draft order as paid (or call `draftOrderComplete`).\n\n```bash\n# 1. Create a draft order from a cart\nDRAFT=$(shop_gql '\nmutation($input: DraftOrderInput!) {\n  draftOrderCreate(input: $input) {\n    draftOrder { id totalPriceSet { shopMoney { amount currencyCode } } invoiceUrl }\n    userErrors { field message }\n  }\n}' '{\"input\":{\"lineItems\":[{\"variantId\":\"gid:\u002F\u002Fshopify\u002FProductVariant\u002F...\",\"quantity\":1}],\"email\":\"buyer@example.com\"}}')\n\nDRAFT_ID=$(echo \"$DRAFT\" | jq -r '.data.draftOrderCreate.draftOrder.id')\nTOTAL=$(echo \"$DRAFT\" | jq -r '.data.draftOrderCreate.draftOrder.totalPriceSet.shopMoney.amount')\n```\n\nYour service then exposes an HTTP 402 endpoint pricing this draft (USDC on Base) — see the upstream x402 spec. Bankr-side, the agent settles it:\n\n```bash\n# Bankr-side (agent): pay the x402 endpoint that fronts the draft order\ncurl -sS -X POST https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fprompt \\\n  -H \"Authorization: Bearer ${BANKR_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"prompt\\\":\\\"call x402 endpoint https:\u002F\u002Fshop.example.com\u002Fx402\u002Fdraft\u002F${DRAFT_ID} and settle in USDC on base\\\"}\"\n```\n\nWhen your x402 server confirms settlement, complete the draft on the Shopify side:\n\n```bash\nshop_gql '\nmutation($id: ID!) {\n  draftOrderComplete(id: $id, paymentPending: false) {\n    draftOrder { order { id name } }\n    userErrors { field message }\n  }\n}' \"{\\\"id\\\":\\\"${DRAFT_ID}\\\"}\"\n```\n\nNotes:\n- Use **`shopMoney`** consistently for x402 pricing so currency conversion stays on your side, not Shopify's.\n- Treat the x402 settlement tx hash as the source of truth — do not complete the draft until you've verified the hash.\n\n## Bridge 3 — Shopify webhook → Bankr Submit (loyalty drop on `ORDERS_PAID`)\n\nBankr has no inbound webhooks. The pattern is: Shopify webhook → your server → HMAC verify → read customer handle metafield → submit a Bankr job → poll.\n\n```bash\n# Subscribe once\nshop_gql '\nmutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) {\n  webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) {\n    webhookSubscription { id topic }\n    userErrors { field message }\n  }\n}' '{\"topic\":\"ORDERS_PAID\",\"sub\":{\"callbackUrl\":\"https:\u002F\u002Fyour.server\u002Fshopify\u002Forders-paid\",\"format\":\"JSON\"}}'\n```\n\nWebhook handler pseudocode:\n\n```bash\n# 1. Verify HMAC (reject if it doesn't match) — use a constant-time comparison in real code\nEXPECTED=$(echo -n \"$REQUEST_BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" -binary | base64)\n[ \"$EXPECTED\" = \"$X_SHOPIFY_HMAC_SHA256\" ] || exit 1\n\n# 2. Pull the buyer's Bankr handle from the order's customer metafield\nCUSTOMER_ID=$(echo \"$REQUEST_BODY\" | jq -r '.customer.admin_graphql_api_id')\nHANDLE=$(shop_gql '\nquery($id: ID!) {\n  customer(id: $id) { metafield(namespace:\"custom\", key:\"handle\") { value } }\n}' \"{\\\"id\\\":\\\"${CUSTOMER_ID}\\\"}\" | jq -r '.data.customer.metafield.value')\n[ -z \"$HANDLE\" ] || [ \"$HANDLE\" = \"null\" ] && exit 0   # silently skip, no handle on file\n\n# 3. Submit the loyalty drop to Bankr\nJOB=$(curl -sS -X POST https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fprompt \\\n  -H \"Authorization: Bearer ${BANKR_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"prompt\\\":\\\"send 100 LOYALTY to ${HANDLE} on base\\\"}\" | jq -r '.job_id')\n\n# 4. Poll until terminal\nwhile :; do\n  STATUS=$(curl -sS \"https:\u002F\u002Fapi.bankr.bot\u002Fagent\u002Fjob\u002F${JOB}\" \\\n    -H \"Authorization: Bearer ${BANKR_API_KEY}\" | jq -r '.status')\n  case \"$STATUS\" in\n    completed|failed|cancelled) break ;;\n  esac\n  sleep 2\ndone\n```\n\nSame shape works for: royalty splits on digital goods, Clanker token airdrops to repeat buyers, treasury sweeps when a sales threshold is hit. Add an idempotency key on the Shopify order id so a replayed webhook cannot trigger a duplicate Bankr submission.\n\n## Bridge 4 — Scope cheatsheet for the Bankr flows\n\nMinimum Admin API scopes per bridge:\n\n| Flow | Required scopes |\n| --- | --- |\n| Identity bridge (read\u002Fwrite handle metafield) | `read_customers`, `write_customers` |\n| x402 checkout via draft orders | `read_customers`, `write_draft_orders`, `read_orders` |\n| Webhook → Bankr loyalty drop | `read_orders`, `read_customers`, plus webhook subscription on `ORDERS_PAID` |\n| Bulk export for an offline reconciliation against onchain tx hashes | `read_products`, `read_orders`, `read_customers` |\n\nToken only needs what the agent will actually use. Don't grant `write_*` scopes to a read-only reconciliation agent.\n\n---\n\n## Credit\n\nCore Shopify content (everything above the \"Bankr Bridges\" header) is adapted, with attribution, from `NousResearch\u002Fhermes-agent` (`optional-skills\u002Fproductivity\u002Fshopify\u002FSKILL.md`), MIT licensed. The Bankr bridge sections are new and contributed under the same MIT license.\n",{"name":5,"tags":45,"emoji":49,"visibility":50,"description":51},[20,33,25,46,32,47,24,48],"graphql","usdc","base","?️","public","Shopify Admin & Storefront GraphQL APIs via curl, with Bankr-native bridges. Manage products, orders, customers, inventory, metafields, webhooks, and bulk ops, then wire merchant data to onchain primitives — store a Bankr-resolvable handle (ENS, Twitter, Farcaster, wallet) on each customer as a metafield, expose Shopify draft orders behind x402 endpoints Bankr settles in USDC, and turn ORDERS_PAID webhooks into Bankr agent jobs for loyalty drops or royalty splits. Triggers: \"Shopify products\", \"Shopify orders\", \"create draft order\", \"loyalty drop on order\", \"x402 checkout\", \"tokengate Shopify\". Core Shopify content adapted with attribution from NousResearch\u002Fhermes-agent (MIT).\n","main","3df1d0d67b30ff15236561f8c0f9f06d33f42dfd","skills\u002Ftrading\u002Fbankrbot-official-bankr-shopify",17,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia","https:\u002F\u002Feth2030.com","Hardcore infra. Building @alt-research and applied cryptography.","2026-11-12 02:28:42",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":73},[63,66,69],{"tool":64,"command":65},"npx","npx skills add jiayaoqijia\u002Fcryptoskill --skill bankr-shopify",{"tool":67,"command":68},"bunx","bunx skills add jiayaoqijia\u002Fcryptoskill --skill bankr-shopify",{"tool":70,"command":71},"pnpm","pnpm dlx skills add jiayaoqijia\u002Fcryptoskill --skill bankr-shopify","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Ftrading\u002Fbankrbot-official-bankr-shopify\u002FSKILL.md",true,[75,81,85],{"path":76,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":79,"sourceHtmlUrl":80},"catalog.json","REFERENCE","参考文档",1528,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Ftrading\u002Fbankrbot-official-bankr-shopify\u002Fcatalog.json",{"path":82,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":83,"sourceHtmlUrl":84},"SOURCE.md",323,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Ftrading\u002Fbankrbot-official-bankr-shopify\u002FSOURCE.md",{"path":86,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":87,"sourceHtmlUrl":88},"TRUST.auto.yaml",2130,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Ftrading\u002Fbankrbot-official-bankr-shopify\u002FTRUST.auto.yaml",[90,119,141,157,170,180],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":94,"licenseFileUrl":95,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":40},"01M712C436EQYE43342Q4E7GG1","recipe-performance","Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold\u002Fwarm\u002Fbackground-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout\u002Fpaint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.","MIT","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FSKILL.md",[],[99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118],"performance","recipe performance","recipe","mm","sentry","harness","app","deterministic","recipes","flow","cold","critical","analysis","tasks","react","browser","layout","screen","network","metamask",{"skillNo":120,"name":121,"nameZh":6,"description":122,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":123,"licenseFileUrl":124,"sourceHtmlUrl":125,"categories":126,"keywords":127,"publishedAt":40},"01M70XT2F8A5CQZ81JBP98KSW3","mexc-spot-websocket","MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FSKILL.md",[],[128,129,130,131,132,50,133,134,135,136,137,138,139,140],"mexc spot websocket","spot","websocket","api","workflows","listen","streams","depth","keys","for","api keys","maintenance","protobuf",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":123,"licenseFileUrl":145,"sourceHtmlUrl":146,"categories":147,"keywords":148,"publishedAt":40},"01M70XT0MBXN3WZY1BKN0CPNKR","mexc-spot-rest","MEXC Spot REST workflows for public data, signed account requests, orders, wallet\u002Fadmin actions, and secure signing without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FSKILL.md",[],[149,129,150,151,152,35,153,138,137,136,154,155,38,50,156,132,131],"mexc spot rest","REST","admin","signing","requests","actions","secure","orders",{"skillNo":158,"name":159,"nameZh":6,"description":160,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":123,"licenseFileUrl":161,"sourceHtmlUrl":162,"categories":163,"keywords":164,"publishedAt":40},"01M70XSYSVZ2N0N91XCAR2CAK0","mexc-futures-websocket","MEXC Futures WebSocket workflows for public streams, private account\u002Forder\u002Fposition streams, ping\u002Fpong, subscriptions, and depth maintenance.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FSKILL.md",[],[165,166,130,167,168,139,137,169,135,134,50,132],"mexc futures websocket","futures","position","ping","subscriptions",{"skillNo":171,"name":172,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":94,"licenseFileUrl":173,"sourceHtmlUrl":174,"categories":175,"keywords":176,"publishedAt":40},"01M6H0ZWHVE5W04WG2DKQE2SQC","0xklen-version-and-refresh-sanctions-lists","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FSKILL.md",[],[177,178,179],"refresh","0xklen version and refresh sanctions lists","version",{"skillNo":181,"name":182,"nameZh":6,"description":183,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":94,"licenseFileUrl":184,"sourceHtmlUrl":185,"categories":186,"keywords":187,"publishedAt":40},"01M6H0Z28Q3R8J0S799GJQWXM3","verify-tests-cover-changed-lines","Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FSKILL.md",[],[188,189,190,191,192,193,194,195,196,197,198],"tests","cover","verify tests cover changed lines","verify","coverage","pr","exercise","use","patch","code","share"]