Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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", "
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 2547% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 1345% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 215% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 383% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 282% | 0% |
> The core Shopify sections of this skill are adapted from the upstream NousResearch/hermes-agent skill (MIT, author: community). The four "Bankr Bridges" sections at the end are new and specific to the Bankr ecosystem.
Work 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.
The 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).
shpat_.SHOPIFY_ACCESS_TOKEN — admin token (starts with shpat_)SHOPIFY_STORE_DOMAIN — my-store.myshopify.com (the permanent myshopify domain, not your custom one)SHOPIFY_API_VERSION — default 2026-01BANKR_API_KEY — for the Bankr bridge sections; generate at https://bankr.bot/api-keys> 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/docs/apps/build/dev-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.
Common scopes by task:
read_products, write_productsread_inventory, write_inventory, read_locationsread_orders, write_orders (30 most recent without read_all_orders)read_customers, write_customersread_draft_orders, write_draft_ordersread_fulfillments, write_fulfillmentshttps://$SHOPIFY_STORE_DOMAIN/admin/api/$SHOPIFY_API_VERSION/graphql.jsonX-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN (NOT Authorization: Bearer)POST, always Content-Type: application/json, body is {"query": "...", "variables": {...}}errors array and per-field userErrors. Always check both.gid://shopify/Product/10079467700516, gid://shopify/Variant/..., gid://shopify/Order/.... Pass these verbatim — don't strip the prefix.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/s restore; Plus = 1000/100.Base curl pattern (reusable):
bashshop_gql() { local query="$1" local variables="${2:-{}}" curl -sS -X POST \ "https://${SHOPIFY_STORE_DOMAIN}/admin/api/${SHOPIFY_API_VERSION:-2026-01}/graphql.json" \ -H "Content-Type: application/json" \ -H "X-Shopify-Access-Token: ${SHOPIFY_ACCESS_TOKEN}" \ --data "$(jq -nc --arg q "$query" --argjson v "$variables" '{query: $q, variables: $v}')" }
Pipe through jq for readable output. -sS keeps errors visible but hides the progress bar.
bashshop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq
bashshop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'
bashshop_gql ' query($q: String!) { products(first: 20, query: $q) { edges { node { id title handle status totalInventory variants(first: 5) { edges { node { id sku price inventoryQuantity } } } } } pageInfo { hasNextPage endCursor } } }' '{"q":"hoodie status:active"}' | jq
Query syntax supports title:, sku:, vendor:, product_type:, status:active, tag:, created_at:>2025-01-01. Full grammar: https://shopify.dev/docs/api/usage/search-syntax
bashshop_gql ' query($cursor: String) { products(first: 100, after: $cursor) { edges { cursor node { id handle } } pageInfo { hasNextPage endCursor } } }' '{"cursor":null}' # subsequent calls: pass the previous endCursor
bashshop_gql ' query($id: ID!) { product(id: $id) { id title handle descriptionHtml tags status variants(first: 20) { edges { node { id sku price compareAtPrice inventoryQuantity selectedOptions { name value } } } } metafields(first: 20) { edges { node { namespace key type value } } } } }' '{"id":"gid://shopify/Product/10079467700516"}' | jq
bashshop_gql ' mutation($input: ProductCreateInput!) { productCreate(product: $input) { product { id handle } userErrors { field message } } }' '{"input":{"title":"Test Hoodie","status":"DRAFT","vendor":"Bankr","productType":"Apparel","tags":["test"]}}'
Variants now have their own mutations in recent versions:
bash# Add variants after creating the product shop_gql ' mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkCreate(productId: $productId, variants: $variants) { productVariants { id sku price } userErrors { field message } } }' '{"productId":"gid://shopify/Product/...","variants":[{"optionValues":[{"optionName":"Size","name":"M"}],"price":"49.00","inventoryItem":{"sku":"HD-M","tracked":true}}]}'
bashshop_gql ' mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id sku price } userErrors { field message } } }' '{"productId":"gid://shopify/Product/...","variants":[{"id":"gid://shopify/ProductVariant/...","price":"55.00"}]}'
read_all_orders)bashshop_gql ' { orders(first: 20, reverse: true, query: "financial_status:paid") { edges { node { id name createdAt displayFinancialStatus displayFulfillmentStatus totalPriceSet { shopMoney { amount currencyCode } } customer { id displayName email } lineItems(first: 10) { edges { node { title quantity sku } } } } } } }' | jq
Useful order query filters: financial_status:paid|pending|refunded, fulfillment_status:unfulfilled|fulfilled, created_at:>2025-01-01, tag:gift, email:foo@example.com.
bashshop_gql ' query($id: ID!) { order(id: $id) { id name email shippingAddress { name address1 address2 city province country zip phone } lineItems(first: 50) { edges { node { title quantity variant { sku } originalUnitPriceSet { shopMoney { amount currencyCode } } } } } transactions { id kind status amountSet { shopMoney { amount currencyCode } } } } }' '{"id":"gid://shopify/Order/...."}' | jq
bash# Search shop_gql ' { customers(first: 10, query: "email:*@example.com") { edges { node { id email displayName numberOfOrders amountSpent { amount currencyCode } } } } }' # Create shop_gql ' mutation($input: CustomerInput!) { customerCreate(input: $input) { customer { id email } userErrors { field message } } }' '{"input":{"email":"test@example.com","firstName":"Test","lastName":"User","tags":["api-created"]}}'
Inventory lives on inventory items tied to variants, quantities tracked per location.
bash# Get inventory for a variant across all locations shop_gql ' query($id: ID!) { productVariant(id: $id) { id sku inventoryItem { id tracked inventoryLevels(first: 10) { edges { node { location { id name } quantities(names: ["available","on_hand","committed"]) { name quantity } } } } } } }' '{"id":"gid://shopify/ProductVariant/..."}'
Adjust stock (delta) — uses inventoryAdjustQuantities:
bashshop_gql ' mutation($input: InventoryAdjustQuantitiesInput!) { inventoryAdjustQuantities(input: $input) { inventoryAdjustmentGroup { reason changes { name delta } } userErrors { field message } } }' '{ "input": { "reason": "correction", "name": "available", "changes": [{"delta": 5, "inventoryItemId": "gid://shopify/InventoryItem/...", "locationId": "gid://shopify/Location/..."}] } }'
Set absolute stock (not delta) — inventorySetQuantities:
bashshop_gql ' mutation($input: InventorySetQuantitiesInput!) { inventorySetQuantities(input: $input) { inventoryAdjustmentGroup { id } userErrors { field message } } }' '{"input":{"reason":"correction","name":"available","ignoreCompareQuantity":true,"quantities":[{"inventoryItemId":"gid://shopify/InventoryItem/...","locationId":"gid://shopify/Location/...","quantity":100}]}}'
Metafields attach custom data to resources (products, customers, orders, shop).
bash# Read shop_gql ' query($id: ID!) { product(id: $id) { metafields(first: 10, namespace: "custom") { edges { node { key type value } } } } }' '{"id":"gid://shopify/Product/..."}' # Write (works for any owner type) shop_gql ' mutation($metafields: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $metafields) { metafields { id key namespace } userErrors { field message code } } }' '{"metafields":[{"ownerId":"gid://shopify/Product/...","namespace":"custom","key":"care_instructions","type":"multi_line_text_field","value":"Wash cold. Tumble dry low."}]}'
Different endpoint, different token, used for customer-facing apps/hydrogen-style headless setups. Headers differ:
https://$SHOPIFY_STORE_DOMAIN/api/$SHOPIFY_API_VERSION/graphql.jsonX-Shopify-Storefront-Access-Token: <public token> — embeddable in browserShopify-Storefront-Private-Token: <private token> — server-onlybashcurl -sS -X POST \ "https://${SHOPIFY_STORE_DOMAIN}/api/${SHOPIFY_API_VERSION:-2026-01}/graphql.json" \ -H "Content-Type: application/json" \ -H "X-Shopify-Storefront-Access-Token: ${SHOPIFY_STOREFRONT_TOKEN}" \ -d '{"query":"{ shop { name } products(first: 5) { edges { node { id title handle } } } }"}' | jq
For dumps larger than rate limits allow (full product catalog, all orders for a year):
bash# 1. Start bulk query shop_gql ' mutation { bulkOperationRunQuery(query: """ { products { edges { node { id title handle variants { edges { node { sku price } } } } } } } """) { bulkOperation { id status } userErrors { field message } } }' # 2. Poll status shop_gql '{ currentBulkOperation { id status errorCode objectCount fileSize url partialDataUrl } }' # 3. When status=COMPLETED, download the JSONL file curl -sS "$URL" > products.jsonl
Each JSONL line is a node, and nested connections are emitted as separate lines with __parentId. Reassemble client-side if needed.
Subscribe to events so you don't have to poll:
bashshop_gql ' mutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) { webhookSubscription { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } } userErrors { field message } } }' '{"topic":"ORDERS_CREATE","sub":{"callbackUrl":"https://example.com/webhook","format":"JSON"}}'
Verify incoming webhook HMAC using the app's client secret (not the access token):
bashecho -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64 # Compare to X-Shopify-Hmac-Sha256 header
/admin/api/.../products.json. Use GraphQL.shpat_. Storefront public tokens with shpua_. If you have one and the wrong header, every request returns 401 without a useful error body.{"errors":[{"message":"Access denied for ..."}]}. Re-configure Admin API scopes on the app, then reinstall to regenerate the token.userErrors is empty != success. Also check data.<mutation>.<resource> is non-null. Some failures populate neither — inspect the whole response.gid://shopify/Product/<numeric>.products(first: 250) with deep nesting can cost 1000+ points and throttle immediately on a standard-plan shop. Start narrow, read extensions.cost, adjust.products(first: N, reverse: true) sorts by id DESC, not created_at. Use sortKey: CREATED_AT, reverse: true for "newest first."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."49.00" not 49.0. Don't jq tonumber blindly if you care about zero-padding.shopMoney (store's currency) AND presentmentMoney (customer's). Pick one consistently.Mutations 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.
The 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.
Per the Bankr docs:
POST /agent/prompt then GET /agent/job/{id}. There are no Bankr-side webhooks.bankr_agent_submit_prompt, bankr_agent_get_job_status, bankr_agent_cancel_job.Shopify'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.
Recommended metafield definition: namespace: custom, key: handle, type: single_line_text_field. Acceptable values: vitalik.eth, @dwr.eth, @username (Twitter), or a 0x… wallet address.
bash# Write a handle onto a customer at checkout / signup shop_gql ' mutation($metafields: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $metafields) { metafields { id key value } userErrors { field message code } } }' '{"metafields":[{"ownerId":"gid://shopify/Customer/123","namespace":"custom","key":"handle","type":"single_line_text_field","value":"vitalik.eth"}]}'
Read it back when you need to pay or drop tokens:
bashHANDLE=$(shop_gql ' query($id: ID!) { customer(id: $id) { metafield(namespace:"custom", key:"handle") { value } } }' '{"id":"gid://shopify/Customer/123"}' | jq -r '.data.customer.metafield.value') # Hand off to Bankr — no extra resolution needed. curl -sS -X POST https://api.bankr.bot/agent/prompt \ -H "Authorization: Bearer ${BANKR_API_KEY}" \ -H "Content-Type: application/json" \ -d "{\"prompt\":\"send 10 USDC to ${HANDLE} on base\"}"
If the metafield is empty, prompt the customer for one of {ENS, Twitter, Farcaster, wallet} before any onchain action — never guess from email.
Bankr 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).
bash# 1. Create a draft order from a cart DRAFT=$(shop_gql ' mutation($input: DraftOrderInput!) { draftOrderCreate(input: $input) { draftOrder { id totalPriceSet { shopMoney { amount currencyCode } } invoiceUrl } userErrors { field message } } }' '{"input":{"lineItems":[{"variantId":"gid://shopify/ProductVariant/...","quantity":1}],"email":"buyer@example.com"}}') DRAFT_ID=$(echo "$DRAFT" | jq -r '.data.draftOrderCreate.draftOrder.id') TOTAL=$(echo "$DRAFT" | jq -r '.data.draftOrderCreate.draftOrder.totalPriceSet.shopMoney.amount')
Your service then exposes an HTTP 402 endpoint pricing this draft (USDC on Base) — see the upstream x402 spec. Bankr-side, the agent settles it:
bash# Bankr-side (agent): pay the x402 endpoint that fronts the draft order curl -sS -X POST https://api.bankr.bot/agent/prompt \ -H "Authorization: Bearer ${BANKR_API_KEY}" \ -H "Content-Type: application/json" \ -d "{\"prompt\":\"call x402 endpoint https://shop.example.com/x402/draft/${DRAFT_ID} and settle in USDC on base\"}"
When your x402 server confirms settlement, complete the draft on the Shopify side:
bashshop_gql ' mutation($id: ID!) { draftOrderComplete(id: $id, paymentPending: false) { draftOrder { order { id name } } userErrors { field message } } }' "{\"id\":\"${DRAFT_ID}\"}"
Notes:
shopMoney consistently for x402 pricing so currency conversion stays on your side, not Shopify's.ORDERS_PAID)Bankr has no inbound webhooks. The pattern is: Shopify webhook → your server → HMAC verify → read customer handle metafield → submit a Bankr job → poll.
bash# Subscribe once shop_gql ' mutation($topic: WebhookSubscriptionTopic!, $sub: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $sub) { webhookSubscription { id topic } userErrors { field message } } }' '{"topic":"ORDERS_PAID","sub":{"callbackUrl":"https://your.server/shopify/orders-paid","format":"JSON"}}'
Webhook handler pseudocode:
bash# 1. Verify HMAC (reject if it doesn't match) — use a constant-time comparison in real code EXPECTED=$(echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" -binary | base64) [ "$EXPECTED" = "$X_SHOPIFY_HMAC_SHA256" ] || exit 1 # 2. Pull the buyer's Bankr handle from the order's customer metafield CUSTOMER_ID=$(echo "$REQUEST_BODY" | jq -r '.customer.admin_graphql_api_id') HANDLE=$(shop_gql ' query($id: ID!) { customer(id: $id) { metafield(namespace:"custom", key:"handle") { value } } }' "{\"id\":\"${CUSTOMER_ID}\"}" | jq -r '.data.customer.metafield.value') [ -z "$HANDLE" ] || [ "$HANDLE" = "null" ] && exit 0 # silently skip, no handle on file # 3. Submit the loyalty drop to Bankr JOB=$(curl -sS -X POST https://api.bankr.bot/agent/prompt \ -H "Authorization: Bearer ${BANKR_API_KEY}" \ -H "Content-Type: application/json" \ -d "{\"prompt\":\"send 100 LOYALTY to ${HANDLE} on base\"}" | jq -r '.job_id') # 4. Poll until terminal while :; do STATUS=$(curl -sS "https://api.bankr.bot/agent/job/${JOB}" \ -H "Authorization: Bearer ${BANKR_API_KEY}" | jq -r '.status') case "$STATUS" in completed|failed|cancelled) break ;; esac sleep 2 done
Same 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.
Minimum Admin API scopes per bridge:
| Flow | Required scopes | | --- | --- | | Identity bridge (read/write handle metafield) | read_customers, write_customers | | x402 checkout via draft orders | read_customers, write_draft_orders, read_orders | | Webhook → Bankr loyalty drop | read_orders, read_customers, plus webhook subscription on ORDERS_PAID | | Bulk export for an offline reconciliation against onchain tx hashes | read_products, read_orders, read_customers |
Token only needs what the agent will actually use. Don't grant write_* scopes to a read-only reconciliation agent.
Core Shopify content (everything above the "Bankr Bridges" header) is adapted, with attribution, from NousResearch/hermes-agent (optional-skills/productivity/shopify/SKILL.md), MIT licensed. The Bankr bridge sections are new and contributed under the same MIT license.
Other measured skills in the registry, with their headline benchmark lift.