Bowmark AIdocs

Dr. Squatch

Dr. Squatch — Every Shopify storefront, reached by its own name.

Dr. Squatch — Every Shopify storefront, reached by its own name. One engine over the platform's two published surfaces — the store's own UCP MCP server at /api/ucp/mcp for search, and the keyless Ajax storefront JSON for products and the cart.

Domain: drsquatch.com

Also known as: drsquatch, Dr. Squatch

Call it directly

bowmark.providers.drsquatch.search(query: string, opts?: { productType?: string; inStockOnly?: boolean; limit?: number }): Promise<ShopifyProduct[]>
bowmark.providers.drsquatch.getProduct(handle: string): Promise<ShopifyProduct>
bowmark.providers.drsquatch.resolveProductUrl(url: string): Promise<ShopifyProductFromUrl>
bowmark.providers.drsquatch.getProducts(handles: string[]): Promise<ShopifyProductBatch>
bowmark.providers.drsquatch.listProducts(opts?: { limit?: number; cursor?: string | null }): Promise<ShopifyProductPage>
bowmark.providers.drsquatch.listCollections(opts?: { limit?: number }): Promise<ShopifyCollection[]>
bowmark.providers.drsquatch.getCollection(handle: string, opts?: { limit?: number; cursor?: string | null }): Promise<ShopifyCollectionProducts>
bowmark.providers.drsquatch.getSetEvidence(handle: string, opts?: { includeAlgorithmic?: boolean }): Promise<ShopifySetEvidence>

Functions

FunctionWhat it does
searchSearches Dr.
getProductReads one Dr.
resolveProductUrlTurns a Dr.
getProductsReads FULL detail for many Dr.
listProductsWalks Dr.
listCollectionsLists Dr.
getCollectionReads one Dr.
getSetEvidenceTwo answers about what goes with this Dr.

Types

interface ShopifyVariant {
  /** Shopify's numeric variant id as a string. What addToCart takes. */
  id: string;
  /** The variant's own label, e.g. "Black / S". */
  title: string;
  /** Decimal string exactly as the store publishes it, e.g. "45.00". */
  price: string;
  compareAtPrice: string | null;
  sku: string | null;
  /** The store's own per-variant stock flag. */
  available: boolean;
  options: string[];
  /** The same values keyed by the option's own NAME — { Color: "Black", Size: "S" }.
   * Read THIS to filter by size; options[1] is only the size on a store that
   * happens to order it second. {} when the two lists cannot be reconciled. */
  selectedOptions: Record<string, string>;
  /** The image the STORE linked to this variant. Usually NULL — most storefronts
   * publish no image-to-variant link at all, and null says so rather than
   * handing back the first product picture. */
  image: ProductImage | null;
  /** Why this price is, or is not, a markdown. */
  sale: SaleEvidence;
}
interface ProductImage {
  /** Absolute HTTPS url, at the largest rendition the CDN serves. */
  url: string;
  /** The retailer's own alt text, or null. */
  altText: string | null;
  /** The retailer's colour name for this picture. Set when the store links it,
   * or when the whole product is ONE colourway (which is how both yoga
   * retailers publish: one colour per product). Null otherwise. */
  color: string | null;
  colorId: string | null;
  /** Variant ids the STORE linked. [] means it published no link — NOT that the
   * image belongs to every variant. */
  variantIds: string[];
}
interface SaleEvidence {
  /** True only with retailer evidence. NEVER set from a low price or from the
   * word "sale" in a title. */
  onSale: boolean;
  currentPrice: string | null;
  /** The published "was" price. Strictly greater than currentPrice when
   * evidenceType is "compare_at_price". Null when nothing published one. */
  originalPrice: string | null;
  /** ISO 4217, from the storefront's own /meta.json. Null only when that read
   * failed — never defaulted to "USD", which is wrong on every non-US store. */
  currency: string | null;
  promotionMessage: string | null;
  /** How it was decided. "compare_at_price" is a struck-through price;
   * "retailer_sale_badge" is a published sale tag; "none" is not on sale. */
  evidenceType: "compare_at_price" | "sale_price" | "retailer_sale_badge" | "published_promotion" | "none";
  /** The retailer text or field the classification rests on, verbatim. */
  evidenceText: string | null;
}
/** The retailer's own labels, read off its published tags, category and options.
 * NEVER inferred from the title or from an image. Null or [] when unpublished,
 * which is common and is an honest answer. */
interface PublishedProductAttributes {
  audience: string | null;
  garmentType: "sports_bra" | "tank" | "crop_top" | "leggings" | "shorts" | "other" | null;
  categories: string[];
  collections: string[];
  fabrics: string[];
  materials: string[];
  color: string | null;
  colorFamily: string | null;
  pattern: string | null;
  styleTags: string[];
  neckline: string | null;
  strapWidth: string | null;
  backDesign: string | null;
  sleeveLength: string | null;
  rise: string | null;
  waistband: string | null;
  /** How long the GARMENT is — "Cropped", "Midi". What every store's own
   * `length::` tag fills, on a top exactly as on a bottom. */
  garmentLength: string | null;
  /** The inside leg seam and only that. NULL on a top — a tank has no inseam,
   * and its length is `garmentLength`. */
  inseam: string | null;
  legShape: string | null;
  fit: string | null;
  coverage: string | null;
}
/** Shared colour, fabric, collection and style family — what makes two garments
 * PLAUSIBLY coordinate. None of it establishes a set. */
interface CoordinationMetadata {
  collectionNames: string[];
  fabricNames: string[];
  colorName: string | null;
  colorId: string | null;
  colorFamily: string | null;
  /** The store's own grouping key for one style across its colourways. Two
   * products sharing it are the same garment in two colours. */
  productFamily: string | null;
}
/** An EXPLICIT retailer-published relationship. Shared colour, fabric,
 * collection or family is NOT this — that is CoordinationMetadata. Usually [].
 *
 * Two doors fill it. On a PRODUCT row it comes from the store's tags, and no
 * store measured publishes a set tag, so it is [] there. getSetEvidence() reads
 * the other one: the complementary products a MERCHANDISER pinned by hand,
 * admitted only for rows the store marks pr_prod_strat=pinned. The algorithmic
 * "related products" feed is never read into this — that is a recommendation
 * engine's output, not the retailer stating a pairing. */
interface RetailerSetEvidence {
  evidenceType: "official_set" | "shop_the_set" | "complete_the_look" | "matching_piece";
  evidenceText: string | null;
  sourceUrl: string | null;
  setId: string | null;
  relatedProducts: Array<{ productId: string | null; handle: string | null; title: string | null; url: string | null }>;
}
/** What getSetEvidence returns. An OBJECT rather than a bare array so the
 * healthy EMPTY answer is still probeable: productId and sourceUrl exist only
 * when BOTH hops answered, where an empty evidence list is the ordinary case. */
interface ShopifySetEvidence {
  handle: string;
  /** Shopify's numeric PRODUCT id — the only key the recommendations door takes,
   * and published by no other function here. */
  productId: string;
  /** The product page the pairing is published on. */
  sourceUrl: string;
  /** ONLY the rows a merchandiser pinned by hand. [] when they pinned nothing,
   * which is the common case and is the STORE's answer. At most one entry — one
   * statement, N related products. Read this for "what did the retailer SAY". */
  evidence: RetailerSetEvidence[];
  /** EVERY row the store recommends, pinned AND algorithmic, deduplicated, each
   * labelled. Read source.by before treating one as the retailer's decision —
   * the algorithmic rows are usually SUBSTITUTES rather than companions, since
   * similarity returns the nearest product and the nearest thing to a baby
   * monitor is another baby monitor. [] when includeAlgorithmic was false. */
  recommendations: RecommendedProduct[];
  /** Non-empty when the store returned a FULL page for an intent, which cannot be
   * told apart from a longer list cut off at the cap. A merchandiser's pairing is
   * a statement, so a partial one must not read as the whole. */
  warnings: string[];
}
/** One recommended product, with the store's own label for who chose it. */
interface RecommendedProduct {
  productId: string | null;
  handle: string | null;
  title: string | null;
  url: string | null;
  source: RecommendationSource;
}
interface RecommendationSource {
  /** "retailer" when a merchandiser pinned it; "algorithm" otherwise, INCLUDING
   * a row the store labelled with nothing — an unknown provenance must never
   * read as a person's decision. */
  by: "retailer" | "algorithm";
  /** The store's own token, verbatim and unmapped — "pinned", "jac" (Jaccard),
   * "e" (embedding), "collection_fallback". Null when the row carried none. */
  strategy: string | null;
}
interface ShopifyProduct {
  handle: string;
  title: string;
  productType: string;
  vendor: string;
  url: string;
  optionNames: string[];
  variants: ShopifyVariant[];
  priceRange: { min: string; max: string } | null;
  inStock: boolean;
  tags: string[];
  descriptionHtml: string | null;
  /** The same copy with its markup removed. */
  descriptionText: string | null;
  /** ISO 4217, read once per store from its own /meta.json. */
  currency: string | null;
  /** When the store's door ANSWERED this row, ISO 8601 UTC. Every field here is
   * a live fact with a shelf life — the price, the markdown, the per-variant
   * stock flag — so read it before treating a cached row as current. Stamped per
   * REQUEST: a getProducts batch carries one stamp per handle, not one per call. */
  fetchedAt: string;
  /** Every image the door published, deduplicated, in the store's own order. */
  images: ProductImage[];
  attributes: PublishedProductAttributes;
  /** Per-FIELD origin for `attributes` — keyed by the same field names. Read it
   * before ranking on a null: "absent" is the store publishing nothing, and is
   * the store's own answer; "unreachable" is a door we could not read, and means
   * UNKNOWN. On this store both doors are one request that either answered or
   * threw, so nothing here is ever "unreachable" — the status exists because the
   * same vocabulary is used by providers whose page can refuse mid-answer. */
  attributeProvenance: Record<string, FieldProvenance>;
  attributeCompleteness: Completeness;
  coordination: CoordinationMetadata;
  retailerSetEvidence: RetailerSetEvidence[];
}
/** Where one attribute value came from, and the retailer text behind it. */
interface FieldProvenance {
  /** "published" — the retailer stated it. "absent" — every door was silent.
   * "unreachable" — a door that would carry it was refused, so it is UNKNOWN. */
  status: "published" | "absent" | "unreachable";
  /** Which door: "shopify_tags", "shopify_product_type", "shopify_color_option"
   * or "shopify_product_copy". Null when nothing filled it. */
  source: string | null;
  /** The retailer's own words the value rests on, verbatim — the tag, or the
   * sentence out of the description. Null when nothing filled it. */
  evidence: string | null;
  /** On "unreachable" only: what was refused. */
  detail?: string;
}
/** The roll-up over one product's attributeProvenance. */
interface Completeness {
  fields: number;
  published: number;
  absent: number;
  unreachable: number;
  /** published / fields, to 3dp. */
  ratio: number;
  /** The fields whose value is UNKNOWN rather than known-empty. Read this before
   * comparing two rows: a row with entries here was not fully looked at. */
  unreachableFields: string[];
  sourcesUsed: string[];
}
/** What getProducts returns. PARTIAL by design: one handle the store will not
 * serve costs that row and nothing else, where getProduct throws. */
interface ShopifyProductBatch {
  /** In the order the handles were passed, not the order they finished. */
  products: ShopifyProduct[];
  /** Every handle the store did not serve, with what it said. */
  missing: Array<{ handle: string; detail: string }>;
  requested: number;
  warnings: string[];
}
interface ShopifyCartLine {
  /** Shopify's own line key, which its cart-change endpoints address a line by. */
  lineKey: string;
  variantId: string;
  productTitle: string;
  /** Null on a single-variant product. */
  variantTitle: string | null;
  quantity: number;
  price: string;
  lineTotal: string;
  url: string;
}
/** One collection the store publishes — its own merchandised grouping, and the
 * ONE place either yoga retailer states a set. Membership in a collection the
 * retailer NAMED "Matching Sets" is a published fact and is admissible as set
 * evidence; two products being adjacent inside it is NOT, because a position in
 * a list is not a statement. */
interface ShopifyCollection {
  handle: string;
  title: string;
  url: string;
  descriptionHtml: string | null;
  /** True when the retailer's OWN title or handle names this a set, a matching
   * piece or a look. Never inferred from what is inside it. */
  setLike: boolean;
  productCount: number | null;
  updatedAt: string | null;
}
interface ShopifyCollectionProducts {
  handle: string;
  url: string;
  /** In the RETAILER's own order. Curated collections are merchandised
   * top-then-bottom; reading a pairing out of that order is the caller's
   * inference, never this provider's claim. [] is an ordinary answer. */
  products: ShopifyProduct[];
  /** Pass back as opts.cursor for the next page, or NULL when this is the last
   * one. A collection bigger than one page is reachable only through this. */
  cursor: string | null;
  /** Empty on an ordinary page. One entry when the walk hit the store platform's
   * 25,000-row ceiling with the collection unfinished — a TRUNCATION, which a
   * null cursor on its own would read as the end of the list. */
  warnings: string[];
}
/** One page of a whole-catalogue walk. Advance it with the cursor; there is
 * deliberately no "fetch everything" call, because every row is a request
 * against the store and only the caller knows how many candidates it needs. */
interface ShopifyProductPage {
  /** In the store's own MERCHANDISED order — not id, not date. The store may
   * re-merchandise mid-walk, so key on handle rather than assuming pages are
   * disjoint. */
  products: ShopifyProduct[];
  /** Pass back as opts.cursor for the next page. NULL when the store answered a
   * short page, which is what the end of the catalogue looks like. */
  cursor: string | null;
  /** How many rows this page asked the store for. */
  limit: number;
  /** Empty on an ordinary page. One entry when the walk stopped at the 25,000-row
   * ceiling with the catalogue unfinished. */
  warnings: string[];
}
/** What resolveProductUrl hands back — the product a url names, and the variant
 * its own ?variant= selected. */
interface ShopifyProductFromUrl {
  product: ShopifyProduct;
  /** The variant ?variant= named, or NULL when the url named none — the ordinary
   * case for a link off a collection page. Also null when it named one the store
   * no longer publishes, which warnings says. NEVER the first variant instead:
   * that answers "is my size in stock" about a different size. */
  variant: ShopifyVariant | null;
  /** The ?variant= value exactly as the url carried it, kept even when it
   * matched nothing — a stale link is a fact about the link. */
  variantIdInUrl: string | null;
  /** Empty on a clean resolve. One entry when the url named a variant the store
   * no longer publishes. */
  warnings: string[];
}
interface ShopifyCart {
  /** The store's own cart token — a bearer credential, so treat it like one. */
  token: string;
  lines: ShopifyCartLine[];
  /** Units, NOT lines: two of one variant is one line and two items. */
  itemCount: number;
  subtotal: string;
  currency: string;
  cartUrl: string;
  checkoutUrl: string;
}

Examples

// Everything published about one product: the size a shopper can actually
// buy, the picture for that colour, and WHY a price is a markdown.
const [hit] = await bowmark.providers.drsquatch.search("leggings", { inStockOnly: true, limit: 1 });
if (!hit) return { note: "nothing matched" };
const p = await bowmark.providers.drsquatch.getProduct(hit.handle);
const small = p.variants.find((v) => v.selectedOptions.Size === "S" && v.available);
return {
  title: p.title,
  // The retailer's OWN labels. null/[] means it published none — never inferred.
  garment: p.attributes.garmentType,      // "leggings" | "sports_bra" | … | null
  fabric: p.attributes.fabrics,           // ["Spacedye"] on Beyond Yoga, [] elsewhere
  colour: p.attributes.color,
  small: small && {
    variantId: small.id,                  // the ONLY thing addToCart takes
    price: small.price,
    onSale: small.sale.onSale,
    why: small.sale.evidenceType,         // "compare_at_price" | "retailer_sale_badge" | "none"
    was: small.sale.originalPrice,        // null unless the store published one
    proof: small.sale.evidenceText,
  },
  // Absolute https. `color` is set when the store links it OR when the whole
  // product is one colourway; `variantIds: []` means NO published link — it does
  // NOT mean the picture is every variant's.
  photos: p.images.map((i) => ({ url: i.url, color: i.color, variantIds: i.variantIds })),
};
// Coordinating two pieces, WITHOUT pretending the store sold them as a set.
const tops = await bowmark.providers.drsquatch.search("tank", { inStockOnly: true, limit: 5 });
const bottoms = await bowmark.providers.drsquatch.search("leggings", { inStockOnly: true, limit: 5 });
// THE STRONGEST claim, and the only one that is the retailer SPEAKING: what a
// merchandiser pinned to this product by hand. Its own call, and [] on most
// products, which is the store's answer rather than a failure.
const set = tops[0] && (await bowmark.providers.drsquatch.getSetEvidence(tops[0].handle));
const pinned = set ? set.evidence : [];   // [] means the merchandiser pinned nothing
// Everything the store recommends, labelled. NEVER treat a row as the retailer's
// decision without reading source.by — the "algorithm" rows are usually
// SUBSTITUTES (a baby monitor's nearest neighbour is another baby monitor).
const alternatives = (set ? set.recommendations : []).filter((r) => r.source.by === "algorithm");
// The product rows carry the TAG-derived half of the same field, which is []
// on every store measured because no store publishes a set:: tag.
const fromTags = [...tops, ...bottoms].flatMap((p) => p.retailerSetEvidence);
// coordination is the weaker, honest signal: shared colour, fabric, collection,
// and the store's own style family. Weigh it; do not report it as a set.
const pairs = tops.flatMap((t) =>
  bottoms
    .filter((b) => b.coordination.colorFamily && b.coordination.colorFamily === t.coordination.colorFamily)
    .map((b) => ({ top: t.title, bottom: b.title, sharedColorFamily: t.coordination.colorFamily })),
);
// pinned[0].relatedProducts is what the retailer SAID. pairs is what we matched.
return { pinned, alternatives, fromTags, coordinated: pairs };