bankr-shopify/SKILL.md
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/hermes-agent (MIT).
npx skillsauth add bankrbot/skills bankr-shopifyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The core Shopify sections of this skill are adapted from the upstream
NousResearch/hermes-agentskill (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-keysHeads 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):
shop_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.
shop_gql '{ shop { name myshopifyDomain primaryDomain { url } currencyCode plan { displayName } } }' | jq
shop_gql '{ publicApiVersions { handle supported } }' | jq '.data.publicApiVersions[] | select(.supported)'
shop_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
shop_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
shop_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
shop_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:
# 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}}]}'
shop_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)shop_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:[email protected].
shop_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
# 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":"[email protected]","firstName":"Test","lastName":"User","tags":["api-created"]}}'
Inventory lives on inventory items tied to variants, quantities tracked per location.
# 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:
shop_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:
shop_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).
# 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-onlycurl -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):
# 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:
shop_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):
echo -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.
# 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:
HANDLE=$(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).
# 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":"[email protected]"}}')
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:
# 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:
shop_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.
# 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:
# 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.
data-ai
Discover, bet on, track, and settle Hunch prediction markets in natural language. Trigger when a user wants to bet, take a position, or get odds on a crypto outcome — token market-cap milestones and flips, launchpad races (Bankr vs pump.fun volume / #1-days / launches over a cap), token head-to-head outperformance, mcap strike-ladders, and up/down price rounds. Also trigger on "what can I bet on about $TOKEN", "odds on …", "take YES/NO on …", "show my Hunch bets", "did my market resolve". Settles in USDC on Base via x402 (≤ $10 / bet); every bet returns an on-chain proof.
tools
HSM-backed secret management for AI agents. Store API keys (including Bankr `bk_` keys), passwords, and credentials in an encrypted vault; retrieve them at runtime via MCP without keeping secrets in chat context. Bankr Dynamic Key Vending issues short-lived scoped `bk_usr_` keys from a partner key (`bk_ptr_`) without manual rotation. Policy-based access control, secret rotation, sharing, EVM transaction intents (sign/simulate/broadcast), multi-chain signing keys, treasury multisig proposals, OIDC federation for external service auth, built-in prompt injection detection, and optional Shroud TEE LLM proxy. Use when the agent needs secure credential storage, just-in-time secret access, guarded on-chain signing, or security scanning — not for Bankr trading prompts, portfolio checks, or x402 calls (use the bankr skill instead).
development
Give your Bankr agent its own brain and a wallet-signed line to every other agent — on any framework, with no API key. SIGNA is the keyless agent layer on Base: resolve any identity to a messageable wallet, send and read wallet-signed DMs, invoke capabilities on the network, and run a brain that reasons on decentralized inference and acts through those capabilities. The Bankr wallet is the only credential. Triggers: "message that agent", "DM this wallet/handle", "reach the agent behind @x", "what is the base market", "resolve @handle to a wallet", "ask the network", "let my agent think and report".
development
AI-powered crypto trading agent, wallet API, and LLM gateway via natural language. Use when the user wants to trade crypto, check portfolio balances (with PnL and NFTs), view token prices, search tokens, transfer crypto, manage NFTs, use leverage (Hyperliquid or Avantis), bet on Polymarket, deploy tokens, set up automated trading, sign and submit raw transactions, call or deploy x402 paid API endpoints, browse the web, or access LLM models through the Bankr LLM gateway funded by your Bankr wallet. Supports Base, Ethereum, Polygon, Solana, Unichain, World Chain, Arbitrum, and BNB Chain.