@curless/shopify-adapter
v0.17.0
Published
Use a Shopify store as an agentbank catalog source, and record agent-settled sales back as paid Shopify orders. Maps Shopify Admin products → a generic CatalogItem your merchant backend prices from, writes an order back after an agent pays through agentba
Readme
@curless/shopify-adapter
Use a Shopify store as your agentbank catalog, and record agent-settled sales back as paid Shopify orders. Drop it into a merchant backend so an AI agent can buy your Shopify products through any agentbank payment protocol — the money settles through agentbank, and the sale shows up in your Shopify admin.
Zero runtime dependencies — it uses the platform fetch and the Shopify Admin
GraphQL API. Pairs with @curless/agentbank-merchant-sdk,
which prices and opens the checkout.
npm install @curless/shopify-adapterThe three halves
import {
shopifyConfigFromEnv,
fetchShopifyCatalog,
writebackShopifyOrder,
refundShopifyOrder,
} from '@curless/shopify-adapter';
const shopify = shopifyConfigFromEnv(); // null unless SHOPIFY_* env is set1. Catalog — map the store's products into generic CatalogItems you price from:
const items = shopify ? await fetchShopifyCatalog(shopify) : [];
// items: { sku, name, priceMinor, currency, description, imageUrl?, variantId?, productType? }[]
// …price a merchant-quoted checkout from `items` with the merchant SDK…2. Writeback — after the agent pays through agentbank, record the paid order — every line of it:
await writebackShopifyOrder(shopify, {
lines: [
// CatalogItem.variantId (or its gid), and the amount actually collected per unit
{ variantId: '640', quantity: 2, unitPriceMinor: 1899 },
{ variantId: '641', quantity: 1, unitPriceMinor: 2599 },
],
currency: 'EUR',
protocol: 'acp', // which protocol settled it
agentbankOrderId: [`order_${paymentIntentId}`, paymentIntentId], // idempotency tag candidates
});No money moves in Shopify. The order is booked with one SALE transaction on the
agentbank gateway, for the sum of the lines (not just financialStatus: PAID, which
paints the label and creates no transaction — such an order can never be refunded) and
tagged agentbank + protocol:… + the first agentbank id, so it's traceable and never
mistaken for a Shopify Payments sale. Price every line (plus currency) or none:
without prices there is no total to book the transaction with and the order won't be
reversible, and a partly priced order is refused.
Idempotent across ids. Every agentbankOrderId candidate is searched before
anything is created, so pass every id the sale is known by. Shopify's tag search lags
creation by the search-index refresh — two writebacks of the same sale fired within a
second can both miss; guard in-process on the payment id ahead of this call and treat
the tag search as the cross-restart backstop.
0.15.0 is breaking:
variantId/quantity/unitPriceMinormoved intolines[], andagentbankOrderIdis required (a string or a list of candidates).
3. Refund — when agentbank refunds the sale, stop the Shopify order reading as paid:
// e.g. from your `order.refunded` webhook handler
await refundShopifyOrder(shopify, {
agentbankOrderId: [`order_${paymentIntentId}`, paymentIntentId], // any tag you wrote back with
reason: 'guest cancelled',
});
// → { refunded: true, id } | { refunded: false, reason }It records the reversal rather than processing one — the sale was booked on our
own gateway, so the matching REFUND transaction moves no real money. It does have to
BE a transaction, though: a refund with none attached is a 0.00 refund and the order
goes on reading PAID. An order written back before this adapter recorded a sale
transaction has no parent to refund against and comes back
{refunded:false, reason:'…no SALE transaction…'} — reverse those in the admin once.
Full-order only — agentbank refunds the whole captured amount, and inventing a partial here would let you record a reversal that doesn't match the money that actually went back.
Notes
Auth — pick by whose store it is.
- A store you do NOT own (a real merchant): they create a custom app in
their own admin and give you its Admin API token. Set
SHOPIFY_ADMIN_TOKENSHOPIFY_SHOP_DOMAIN. This is the only option — the client-credentials grant requires the app and shop to sit in the same Shopify organization, so it cannot reach a third party's store at all.
- Your own store: Shopify's 2026 client-credentials grant. A Dev
Dashboard app no longer shows a token in the UI, so you exchange
SHOPIFY_CLIENT_ID+SHOPIFY_CLIENT_SECRETfor a short-lived Admin token programmatically.
A token, when present, wins — no exchange, one fewer call and one fewer way to fail. The app needs
read_products(catalog) andwrite_orders(writeback) — required scopes go inshopify.app.toml, thendeploy+ reinstall. Secrets come from the environment; never hardcode them.- A store you do NOT own (a real merchant): they create a custom app in
their own admin and give you its Admin API token. Set
Amounts are currency-aware. Prices convert to integer minor units using the currency's own exponent — cents for USD/EUR, none for JPY, mills for KWD — so a non-2-decimal store isn't silently mis-scaled.
Record the real amount. Pass
unitPriceMinor+currencyto the writeback so the Shopify order reflects what the agent actually paid. Omit them to fall back to the variant's current price (which drifts if the price changed after payment).Idempotency. The writeback searches for an order already tagged with the agentbank order id. Shopify's tag search lags creation by the search-index refresh, so two writebacks fired within a second can both miss and duplicate — add your own in-process guard on the order id if you fire them rapidly; the tag search is the cross-restart backstop.
Refunds are idempotent and safe to miss.
refundShopifyOrdertakes one id or a list, tries them in order, and returns{refunded:false, reason}— never throws — when nothing matches or the order is already REFUNDED. Ids that could false-match a Shopify search (spaces, operators) are dropped rather than searched: refunding the wrong order is worse than refunding none.Pagination. Products paginate to completion. A single product with more than 250 variants has its tail dropped with a warning (not silently).
MIT
