Skip to main content
The fastest way to sell on ZAM: create a Cloudflare Worker with three endpoints, deploy, and auto-import. Request verification is built in from the start.

Quick start

Create a new project:
mkdir my-service && cd my-service
npm init -y
npm install zam-verify
npm install --save-dev wrangler typescript @cloudflare/workers-types
Add scripts to package.json:
{
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy",
    "typecheck": "tsc --noEmit"
  }
}
Create wrangler.jsonc:
{
  "name": "my-service",
  "main": "src/index.ts",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"]
}
Create tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"],
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

The three endpoints

Every ZAM-compatible service exposes three endpoints:
EndpointPurpose
GET /Returns service name and description
GET /contractMachine-readable contract for ZAM (schema, pricing, examples)
POST /runRuns your service logic (verified with HMAC)

Write your service

Create src/index.ts:
import { verifyZamRequest } from "zam-verify";

// --- Configuration ---

const SERVICE_NAME = "My Translation Service";
const SERVICE_DESCRIPTION = "Translates text between languages.";
const PRICE_AMOUNT_CENTS = 50;

const INPUT_SCHEMA = {
  type: "object",
  properties: {
    text: { type: "string" },
    targetLanguage: { type: "string" },
  },
  required: ["text", "targetLanguage"],
};

const OUTPUT_SCHEMA = {
  type: "object",
  properties: {
    translated: { type: "string" },
  },
};

// --- Contract ---

function buildContract(baseUrl: string) {
  return {
    provider: {
      title: SERVICE_NAME,
      description: SERVICE_DESCRIPTION,
      category: "data",
      tags: ["translation", "language"],
      price: { currency: "USD", amountCents: PRICE_AMOUNT_CENTS, unit: "call" },
      runContract: {
        method: "POST",
        endpointPath: `${baseUrl}/run`,
        healthEndpoint: `${baseUrl}/health`,
        inputSchema: INPUT_SCHEMA,
        outputSchema: OUTPUT_SCHEMA,
      },
    },
  };
}

// --- Business logic ---

async function handleRun(body: unknown): Promise<object> {
  const { text, targetLanguage } = body as { text: string; targetLanguage: string };
  // Your implementation here
  return { translated: `[${targetLanguage}] ${text}` };
}

// --- Worker ---

interface Env {
  ZAM_SIGNING_SECRET?: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const baseUrl = `${url.protocol}//${url.host}`;
    const headers = {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    };

    if (url.pathname === "/" && request.method === "GET") {
      return new Response(
        JSON.stringify({ name: SERVICE_NAME, description: SERVICE_DESCRIPTION }),
        { headers }
      );
    }

    if (url.pathname === "/contract" && request.method === "GET") {
      return new Response(JSON.stringify(buildContract(baseUrl)), { headers });
    }

    if (url.pathname === "/health" && request.method === "GET") {
      return new Response(JSON.stringify({ status: "healthy" }), { headers });
    }

    if (url.pathname === "/run") {
      // HEAD/GET must return 200 — ZAM checks endpoint reachability during review
      if (request.method === "HEAD" || request.method === "GET") {
        return new Response(null, { status: 200, headers });
      }

      // Verify the request came from ZAM (skip in local dev if no secret is set)
      const body = await request.text();
      if (env.ZAM_SIGNING_SECRET) {
        const signature = request.headers.get("X-ZAM-Signature");
        const payload = request.headers.get("X-ZAM-Payload");

        if (!signature || !payload) {
          return new Response(JSON.stringify({ error: "Missing ZAM headers" }), {
            status: 401, headers,
          });
        }

        const result = verifyZamRequest({
          signature,
          payload,
          body,
          signingSecret: env.ZAM_SIGNING_SECRET,
        });

        if (!result.valid) {
          return new Response(JSON.stringify({ error: result.error }), {
            status: 401, headers,
          });
        }
      }

      const input = JSON.parse(body);
      const result = await handleRun(input);
      return new Response(JSON.stringify(result), { headers });
    }

    return new Response(JSON.stringify({ error: "Not found" }), {
      status: 404,
      headers,
    });
  },
} satisfies ExportedHandler<Env>;
When ZAM_SIGNING_SECRET is not set (e.g., during local development), signature verification is skipped so you can test with curl directly. In production, always set the secret.

Develop and test

npm run dev    # local dev server via wrangler
Test the endpoints:
# Check the contract
curl http://localhost:8787/contract | jq

# Test a run (no signing needed locally)
curl -X POST http://localhost:8787/run \
  -H "Content-Type: application/json" \
  -d '{"text": "hello", "targetLanguage": "es"}'

Deploy

npx wrangler login              # authenticate with Cloudflare (one-time)
npm run deploy                   # deploy the worker
Your service goes live at https://<name>.<your-subdomain>.workers.dev.

List on ZAM

Import your service by giving ZAM the worker URL:
zam providers create-from-service https://my-service.your-subdomain.workers.dev
ZAM calls GET /contract, reads the schema and pricing, and creates a provider automatically. After a brief automated review, your provider goes live on the marketplace.

Set up your signing secret

After your provider is published, generate a signing secret and add it to your worker:
# Get your signing secret from ZAM
zam listings get-secret PROVIDER_ID

# Add it to your Cloudflare Worker as a secret
npx wrangler secret put ZAM_SIGNING_SECRET
# Paste the whsec_... value when prompted
Once set, your service will reject any request that isn’t signed by ZAM. See Secure Your Service for more details on how verification works.