agentsclimarketplace

Marketplace integration helper

Skill WesleySmits/agent-skills/.agent/skills/marketplace-integration-helper

43 production-ready skills for AI coding agents. Works with Claude, GitHub Copilot, Cursor, Windsurf, and Zed.

Install
npx -y skills add WesleySmits/agent-skills --skill marketplace-integration-helper

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Generates boilerplate for e-commerce API integrations. Use when the user asks about WooCommerce, Shopify, Bol.com, or Etsy APIs, webhooks, product sync, or order handling.

SKILL.md

17.4 KB, ~4.3k tokens by cl100k_base, as published. Nobody here has run it

Marketplace Integration Helper

When to use this skill

  • User asks to integrate with WooCommerce or Shopify
  • User needs webhook handlers for orders or products
  • User mentions Bol.com, Etsy, or marketplace APIs
  • User wants rate-limited API clients
  • User asks about product synchronization

Workflow

  • Identify target marketplace(s)
  • Generate API client with auth
  • Add rate limiting and retry logic
  • Create webhook handlers
  • Add TypeScript types
  • Implement error recovery

Instructions

Step 1: Identify Marketplace

PlatformAuth TypeRate LimitDocs
ShopifyOAuth / Access Token2 req/sec (burst 40)Admin API
WooCommerceOAuth 1.0 / API Keys25 req/secREST API v3
Bol.comOAuth 2.0 Client Credentials25 req/10secRetailer API
EtsyOAuth 2.0 PKCE10 req/secOpen API v3

Step 2: Base API Client Structure

// lib/marketplace/base-client.ts
interface RateLimitConfig {
  maxRequests: number;
  windowMs: number;
}

interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
}

export abstract class BaseMarketplaceClient {
  protected baseUrl: string;
  protected rateLimitConfig: RateLimitConfig;
  protected retryConfig: RetryConfig;
  private requestQueue: Array<() => Promise<unknown>> = [];
  private processing = false;

  constructor(
    baseUrl: string,
    rateLimit: RateLimitConfig,
    retry: RetryConfig = {
      maxRetries: 3,
      baseDelayMs: 1000,
      maxDelayMs: 10000,
    },
  ) {
    this.baseUrl = baseUrl;
    this.rateLimitConfig = rateLimit;
    this.retryConfig = retry;
  }

  protected abstract getAuthHeaders(): Record<string, string>;

  protected async request<T>(
    method: string,
    path: string,
    body?: unknown,
  ): Promise<T> {
    return this.enqueue(() => this.executeWithRetry<T>(method, path, body));
  }

  private async executeWithRetry<T>(
    method: string,
    path: string,
    body?: unknown,
    attempt = 0,
  ): Promise<T> {
    try {
      const response = await fetch(`${this.baseUrl}${path}`, {
        method,
        headers: {
          "Content-Type": "application/json",
          ...this.getAuthHeaders(),
        },
        body: body ? JSON.stringify(body) : undefined,
      });

      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get("Retry-After") || "5");
        await this.delay(retryAfter * 1000);
        return this.executeWithRetry(method, path, body, attempt);
      }

      if (!response.ok) {
        throw new MarketplaceError(response.status, await response.text());
      }

      return response.json();
    } catch (error) {
      if (attempt < this.retryConfig.maxRetries && this.isRetryable(error)) {
        const delay = Math.min(
          this.retryConfig.baseDelayMs * Math.pow(2, attempt),
          this.retryConfig.maxDelayMs,
        );
        await this.delay(delay);
        return this.executeWithRetry(method, path, body, attempt + 1);
      }
      throw error;
    }
  }

  private isRetryable(error: unknown): boolean {
    if (error instanceof MarketplaceError) {
      return [408, 429, 500, 502, 503, 504].includes(error.status);
    }
    return error instanceof TypeError; // Network errors
  }

  private enqueue<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.requestQueue.push(async () => {
        try {
          resolve(await fn());
        } catch (e) {
          reject(e);
        }
      });
      this.processQueue();
    });
  }

  private async processQueue(): Promise<void> {
    if (this.processing) return;
    this.processing = true;

    while (this.requestQueue.length > 0) {
      const batch = this.requestQueue.splice(
        0,
        this.rateLimitConfig.maxRequests,
      );
      await Promise.all(batch.map((fn) => fn()));
      if (this.requestQueue.length > 0) {
        await this.delay(this.rateLimitConfig.windowMs);
      }
    }

    this.processing = false;
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

export class MarketplaceError extends Error {
  constructor(
    public status: number,
    public body: string,
  ) {
    super(`Marketplace API error ${status}: ${body}`);
  }
}

Step 3: Platform-Specific Clients

Shopify Client:

// lib/marketplace/shopify-client.ts
import { BaseMarketplaceClient } from "./base-client";

interface ShopifyProduct {
  id: number;
  title: string;
  body_html: string;
  vendor: string;
  product_type: string;
  variants: ShopifyVariant[];
  images: ShopifyImage[];
  status: "active" | "archived" | "draft";
}

interface ShopifyVariant {
  id: number;
  product_id: number;
  title: string;
  price: string;
  sku: string;
  inventory_quantity: number;
}

interface ShopifyImage {
  id: number;
  src: string;
  alt: string | null;
}

interface ShopifyOrder {
  id: number;
  order_number: number;
  email: string;
  financial_status: string;
  fulfillment_status: string | null;
  line_items: ShopifyLineItem[];
  total_price: string;
  currency: string;
}

interface ShopifyLineItem {
  id: number;
  product_id: number;
  variant_id: number;
  quantity: number;
  price: string;
}

export class ShopifyClient extends BaseMarketplaceClient {
  private accessToken: string;

  constructor(shop: string, accessToken: string) {
    super(`https://${shop}.myshopify.com/admin/api/2024-01`, {
      maxRequests: 2,
      windowMs: 1000,
    });
    this.accessToken = accessToken;
  }

  protected getAuthHeaders(): Record<string, string> {
    return { "X-Shopify-Access-Token": this.accessToken };
  }

  // Products
  async getProducts(limit = 50): Promise<ShopifyProduct[]> {
    const data = await this.request<{ products: ShopifyProduct[] }>(
      "GET",
      `/products.json?limit=${limit}`,
    );
    return data.products;
  }

  async getProduct(id: number): Promise<ShopifyProduct> {
    const data = await this.request<{ product: ShopifyProduct }>(
      "GET",
      `/products/${id}.json`,
    );
    return data.product;
  }

  async createProduct(
    product: Partial<ShopifyProduct>,
  ): Promise<ShopifyProduct> {
    const data = await this.request<{ product: ShopifyProduct }>(
      "POST",
      "/products.json",
      { product },
    );
    return data.product;
  }

  async updateProduct(
    id: number,
    product: Partial<ShopifyProduct>,
  ): Promise<ShopifyProduct> {
    const data = await this.request<{ product: ShopifyProduct }>(
      "PUT",
      `/products/${id}.json`,
      { product },
    );
    return data.product;
  }

  // Inventory
  async updateInventory(
    inventoryItemId: number,
    locationId: number,
    quantity: number,
  ): Promise<void> {
    await this.request("POST", "/inventory_levels/set.json", {
      inventory_item_id: inventoryItemId,
      location_id: locationId,
      available: quantity,
    });
  }

  // Orders
  async getOrders(status = "any", limit = 50): Promise<ShopifyOrder[]> {
    const data = await this.request<{ orders: ShopifyOrder[] }>(
      "GET",
      `/orders.json?status=${status}&limit=${limit}`,
    );
    return data.orders;
  }

  async fulfillOrder(orderId: number, trackingNumber?: string): Promise<void> {
    await this.request("POST", `/orders/${orderId}/fulfillments.json`, {
      fulfillment: {
        tracking_number: trackingNumber,
        notify_customer: true,
      },
    });
  }
}

WooCommerce Client:

// lib/marketplace/woocommerce-client.ts
import { BaseMarketplaceClient } from "./base-client";
import crypto from "crypto";

interface WooProduct {
  id: number;
  name: string;
  slug: string;
  type: "simple" | "variable" | "grouped";
  status: "publish" | "draft" | "pending";
  sku: string;
  price: string;
  regular_price: string;
  stock_quantity: number | null;
  images: { id: number; src: string; alt: string }[];
}

interface WooOrder {
  id: number;
  status: string;
  currency: string;
  total: string;
  billing: WooAddress;
  shipping: WooAddress;
  line_items: WooLineItem[];
}

interface WooAddress {
  first_name: string;
  last_name: string;
  address_1: string;
  city: string;
  postcode: string;
  country: string;
}

interface WooLineItem {
  id: number;
  product_id: number;
  quantity: number;
  total: string;
}

export class WooCommerceClient extends BaseMarketplaceClient {
  private consumerKey: string;
  private consumerSecret: string;

  constructor(siteUrl: string, consumerKey: string, consumerSecret: string) {
    super(`${siteUrl}/wp-json/wc/v3`, { maxRequests: 25, windowMs: 1000 });
    this.consumerKey = consumerKey;
    this.consumerSecret = consumerSecret;
  }

  protected getAuthHeaders(): Record<string, string> {
    const auth = Buffer.from(
      `${this.consumerKey}:${this.consumerSecret}`,
    ).toString("base64");
    return { Authorization: `Basic ${auth}` };
  }

  // Products
  async getProducts(page = 1, perPage = 100): Promise<WooProduct[]> {
    return this.request("GET", `/products?page=${page}&per_page=${perPage}`);
  }

  async getProduct(id: number): Promise<WooProduct> {
    return this.request("GET", `/products/${id}`);
  }

  async createProduct(product: Partial<WooProduct>): Promise<WooProduct> {
    return this.request("POST", "/products", product);
  }

  async updateProduct(
    id: number,
    product: Partial<WooProduct>,
  ): Promise<WooProduct> {
    return this.request("PUT", `/products/${id}`, product);
  }

  async updateStock(productId: number, quantity: number): Promise<WooProduct> {
    return this.updateProduct(productId, { stock_quantity: quantity });
  }

  // Orders
  async getOrders(status?: string, page = 1): Promise<WooOrder[]> {
    const query = status ? `&status=${status}` : "";
    return this.request("GET", `/orders?page=${page}${query}`);
  }

  async updateOrderStatus(orderId: number, status: string): Promise<WooOrder> {
    return this.request("PUT", `/orders/${orderId}`, { status });
  }
}

See examples/bol-etsy-clients.md for Bol.com and Etsy implementations.

Step 4: Webhook Handlers

Webhook verification and routing:

// lib/marketplace/webhooks.ts
import crypto from "crypto";

interface WebhookHandler<T = unknown> {
  topic: string;
  handler: (payload: T) => Promise<void>;
}

// Shopify webhook verification
export function verifyShopifyWebhook(
  body: string,
  hmacHeader: string,
  secret: string,
): boolean {
  const hash = crypto
    .createHmac("sha256", secret)
    .update(body, "utf8")
    .digest("base64");
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmacHeader));
}

// WooCommerce webhook verification
export function verifyWooCommerceWebhook(
  body: string,
  signature: string,
  secret: string,
): boolean {
  const hash = crypto
    .createHmac("sha256", secret)
    .update(body, "utf8")
    .digest("base64");
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature));
}

// Generic webhook router
export class WebhookRouter {
  private handlers: Map<string, WebhookHandler["handler"]> = new Map();

  register<T>(topic: string, handler: (payload: T) => Promise<void>): void {
    this.handlers.set(topic, handler as WebhookHandler["handler"]);
  }

  async route(topic: string, payload: unknown): Promise<void> {
    const handler = this.handlers.get(topic);
    if (!handler) {
      console.warn(`No handler registered for topic: ${topic}`);
      return;
    }
    await handler(payload);
  }
}

Next.js API route example:

// app/api/webhooks/shopify/route.ts
import { NextRequest, NextResponse } from "next/server";
import {
  verifyShopifyWebhook,
  WebhookRouter,
} from "@/lib/marketplace/webhooks";
import {
  handleOrderCreated,
  handleProductUpdated,
} from "@/lib/marketplace/handlers";

const router = new WebhookRouter();
router.register("orders/create", handleOrderCreated);
router.register("products/update", handleProductUpdated);

export async function POST(request: NextRequest) {
  const body = await request.text();
  const hmac = request.headers.get("X-Shopify-Hmac-Sha256") || "";
  const topic = request.headers.get("X-Shopify-Topic") || "";

  if (!verifyShopifyWebhook(body, hmac, process.env.SHOPIFY_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  try {
    await router.route(topic, JSON.parse(body));
    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("Webhook processing error:", error);
    return NextResponse.json({ error: "Processing failed" }, { status: 500 });
  }
}

Webhook handler implementations:

// lib/marketplace/handlers.ts
import { db } from "@/lib/db";

interface ShopifyOrderPayload {
  id: number;
  order_number: number;
  email: string;
  line_items: Array<{
    product_id: number;
    variant_id: number;
    quantity: number;
  }>;
}

export async function handleOrderCreated(
  payload: ShopifyOrderPayload,
): Promise<void> {
  // Idempotency check
  const existing = await db.order.findUnique({
    where: { externalId: `shopify_${payload.id}` },
  });
  if (existing) return;

  // Create local order record
  await db.order.create({
    data: {
      externalId: `shopify_${payload.id}`,
      platform: "shopify",
      orderNumber: String(payload.order_number),
      customerEmail: payload.email,
      status: "pending",
      lineItems: {
        create: payload.line_items.map((item) => ({
          externalProductId: String(item.product_id),
          externalVariantId: String(item.variant_id),
          quantity: item.quantity,
        })),
      },
    },
  });

  // Sync inventory across platforms
  for (const item of payload.line_items) {
    await syncInventoryAcrossPlatforms(item.product_id, -item.quantity);
  }
}

export async function handleProductUpdated(payload: {
  id: number;
  title: string;
}): Promise<void> {
  await db.product.updateMany({
    where: { externalId: `shopify_${payload.id}` },
    data: { title: payload.title, updatedAt: new Date() },
  });
}

Step 5: Product Sync Service

// lib/marketplace/sync-service.ts
import { ShopifyClient } from "./shopify-client";
import { WooCommerceClient } from "./woocommerce-client";

interface NormalizedProduct {
  sku: string;
  title: string;
  description: string;
  price: number;
  quantity: number;
  images: string[];
}

export class ProductSyncService {
  constructor(
    private shopify: ShopifyClient,
    private woocommerce: WooCommerceClient,
  ) {}

  async syncProductToAll(product: NormalizedProduct): Promise<void> {
    const results = await Promise.allSettled([
      this.syncToShopify(product),
      this.syncToWooCommerce(product),
    ]);

    for (const result of results) {
      if (result.status === "rejected") {
        console.error("Sync failed:", result.reason);
      }
    }
  }

  private async syncToShopify(product: NormalizedProduct): Promise<void> {
    const existing = await this.findShopifyProductBySku(product.sku);

    if (existing) {
      await this.shopify.updateProduct(existing.id, {
        title: product.title,
        body_html: product.description,
        variants: [{ ...existing.variants[0], price: String(product.price) }],
      });
    } else {
      await this.shopify.createProduct({
        title: product.title,
        body_html: product.description,
        variants: [{ sku: product.sku, price: String(product.price) }] as any,
        images: product.images.map((src) => ({ src })) as any,
      });
    }
  }

  private async syncToWooCommerce(product: NormalizedProduct): Promise<void> {
    // Similar pattern for WooCommerce
  }

  private async findShopifyProductBySku(sku: string) {
    const products = await this.shopify.getProducts(250);
    return products.find((p) => p.variants.some((v) => v.sku === sku));
  }
}

Environment Setup

# .env.local
SHOPIFY_SHOP=your-store
SHOPIFY_ACCESS_TOKEN=shpat_xxxxx
SHOPIFY_WEBHOOK_SECRET=xxxxx

WOOCOMMERCE_URL=https://your-store.com
WOOCOMMERCE_KEY=ck_xxxxx
WOOCOMMERCE_SECRET=cs_xxxxx

BOL_CLIENT_ID=xxxxx
BOL_CLIENT_SECRET=xxxxx

ETSY_API_KEY=xxxxx
ETSY_SHARED_SECRET=xxxxx

Validation

Before completing:

  • API client handles rate limits correctly
  • Webhook signatures verified before processing
  • Idempotency checks prevent duplicate processing
  • Error recovery with exponential backoff works
  • TypeScript types match API responses

Error Handling

  • Rate limit exceeded: Queue requests and respect Retry-After header.
  • Authentication failure: Check token expiry; refresh OAuth tokens if needed.
  • Webhook signature mismatch: Reject immediately; log for investigation.
  • Partial sync failure: Use Promise.allSettled; continue with working platforms.
  • Network timeout: Retry with exponential backoff up to max retries.

Resources

What ships with it: 1 file

11.7 KB alongside SKILL.md

examples/

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.