A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
Etsy sellers receive hundreds of buyer messages daily, and without automated protection, they risk leaking personal data or falling for phishing scams. This recipe builds a security guardrail pipeline that intercepts incoming Etsy marketplace messages, scans them for PII using @presidio-dev/hai-guardrails, classifies abusive and phishing content via Mistral AI’s moderation model, and quarantines flagged messages before they reach the seller. A budget tracker from @reaatech/agent-budget-engine keeps token usage under control, and a Next.js dashboard lets you review quarantined messages. You’ll deploy this as a Hono webhook endpoint inside a Next.js application.
An Etsy shop with webhook access (or use the included mock for local testing)
Basic familiarity with TypeScript, Next.js App Router, and REST APIs
Required environment variables (you’ll set these up in Step 2):
MISTRAL_API_KEY — your Mistral AI API key
ETSY_WEBHOOK_SECRET — secret for validating Etsy webhook signatures
LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY — Langfuse observability keys (optional)
Step 1: Create the Next.js project and install dependencies
Start a new Next.js 16 project with the App Router, then install all the packages this recipe needs. Pin every version exactly — no caret or tilde ranges.
Expected output: After pnpm install, you should see a node_modules/ directory and a pnpm-lock.yaml file with zero resolution errors.
Step 2: Configure environment variables
Create a .env.example file at the project root. The recipe reads these variables at runtime — never commit real values to version control.
env
# Env vars used by mistral-ai-security-guardrails-for-etsy-smb-marketplace-messaging.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=developmentMISTRAL_API_KEY=<your-mistral-api-key>ETSY_WEBHOOK_SECRET=<your-etsy-webhook-secret>GUARDRAIL_CHAIN_BUDGET_MAX_LATENCY_MS=1000GUARDRAIL_CHAIN_BUDGET_MAX_TOKENS=8000LANGFUSE_PUBLIC_KEY=<your-langfuse-public-key>LANGFUSE_SECRET_KEY=<your-langfuse-secret-key>LANGFUSE_BASE_URL=<your-langfuse-base-url>BUDGET_SCOPE_KEY=default-sellerBUDGET_LIMIT_USD=10.0
Copy this to a .env.local file and fill in your real values:
terminal
cp .env.example .env.local
Expected output: You now have .env.local with values like MISTRAL_API_KEY=abc123... and ETSY_WEBHOOK_SECRET=your-secret-here.
Step 3: Define shared types
Create the type definitions that the rest of the recipe uses. These interfaces describe Etsy messages, guardrail scan outcomes, quarantine entries, and budget state.
export type { EtsyMessage } from "./message.js";export type { ScanFlag, ScanOutcome, QuarantineEntry, QuarantineStatus } from "./guardrail.js";export type { BudgetState, BudgetStateValue } from "./budget.js";
Expected output: No errors when you run pnpm typecheck. The barrel file re-exports every type so you can import from ../types/index.js anywhere.
Step 4: Create the PII scanner
The PII scanner uses @presidio-dev/hai-guardrails to detect sensitive information like emails, phone numbers, and credit card numbers in message content. It wraps the GuardrailsEngine with a fail-safe — if the engine throws, the scanner defaults to a safe pass-through.
Expected output: The PiiScanner class compiles cleanly. When you call scan("Contact me at john@example.com"), it detects the email and returns { passed: false, redacted: "...[EMAIL]...", flaggedTypes: ["pii"] }.
Step 5: Create the Mistral moderation client
The MistralModeration class wraps Mistral AI’s chat completion API with a content moderation prompt. It classifies a message as abuse, phishing, or safe and returns a threat score from 0 to 1. The classify method is wrapped with withRetry from @reaatech/guardrail-chain for automatic retry on transient failures.
Expected output: The class instantiates with new MistralModeration(). On a clean message, classify("Thanks for your order!") returns { isAbuse: false, isPhishing: false, threatScore: 0, reason: "safe" }.
Step 6: Create the content classifier
The ContentClassifier orchestrates the PII scanner and Mistral moderation into a single pipeline. It first runs the PII scan, then passes the redacted content to Mistral for classification, and combines both results into a single ScanOutcome.
Expected output: The ContentClassifier accepts a PiiScanner and MistralModeration in its constructor. A clean message returns { passed: true, flags: [] }. A message containing PII or abuse returns { passed: false, flags: [...] }.
Step 7: Create the guardrail configuration
The configuration layer loads guardrail settings from a YAML file and environment variables using @reaatech/guardrail-chain-config. It also exposes validateConfigSafe for safe schema validation without throwing.
Expected output:loadGuardrailConfig() reads the YAML file and merges env var overrides from GUARDRAIL_CHAIN_BUDGET_MAX_LATENCY_MS and GUARDRAIL_CHAIN_BUDGET_MAX_TOKENS. If the file is missing, it returns the hardcoded default config.
Step 8: Create the guardrail pipeline
The GuardrailPipeline builds a ChainBuilder from @reaatech/guardrail-chain, adds three guardrails (PII redaction with caching, content moderation, and toxicity filtering), and runs each message through the chain with budget pre-checks.
Create src/guardrail/pipe.ts:
ts
import { ChainBuilder, setLogger, ConsoleLogger, generateCorrelationId, GuardrailError, TimeoutError, BudgetExceededError, ValidationError, GuardrailErrorType, type ChainResult,} from "@reaatech/guardrail-chain";import { PIIRedaction, ToxicityFilter, ContentModeration, CachedGuardrail,} from "@reaatech/guardrail-chain-guardrails";import type { EtsyMessage } from "../types/message.js";import { loadGuardrailConfig } from "./config.js";import { MessageBudgetTracker } from "../budget/tracker.js";function
Expected output: A clean message like “Hello, how are you?” passes all guardrails with { success: true }. A message containing an email like “john@example.com” triggers PII redaction — the output contains [EMAIL] in place of the address.
Step 9: Create the budget tracker
The MessageBudgetTracker wraps BudgetController from @reaatech/agent-budget-engine. It defines a budget scope for the Etsy seller, checks whether each request is within budget before processing, and records usage after each guardrail run. It also subscribes to threshold-breach and hard-stop events to send budget alerts.
Expected output: After initializing new MessageBudgetTracker(), getState() returns { spent: 0, remaining: 10, state: "Active" }. After recording usage up to 80% of the limit, the state transitions to "Warned".
Step 10: Create the alert sender and webhook handler
The AlertSender logs structured JSON alerts to the console and maintains an in-memory quarantine store that the dashboard API reads from. It handles three types of alerts: flagged messages, budget breaches, and quarantine notifications.
Now create the webhook handler using Hono. It receives Etsy message events via POST, validates the HMAC-SHA256 signature, runs the guardrail pipeline, and quarantines flagged messages.
Create the webhook types first in src/webhook/types.ts:
Create the webhook handler in src/webhook/etsy.ts:
ts
import { Hono } from "hono";import { HTTPException } from "hono/http-exception";import crypto from "node:crypto";import type { EtsyWebhookPayload } from "./types.js";import type { EtsyMessage } from "../types/message.js";import type { ScanFlag } from "../types/guardrail.js";import { GuardrailPipeline } from "../guardrail/pipe.js";import { AlertSender } from "../alert/sender.js";import type { QuarantineEntry } from "../types/guardrail.js";const app = new Hono();const alertSender = new AlertSender();let pipelineInstance: GuardrailPipeline | null = null;function getPipeline(): GuardrailPipeline { if (!pipelineInstance) { pipelineInstance = new GuardrailPipeline(); } return pipelineInstance;}app.post("/etsy-webhook", async (c) => { try { const rawBody = await c.req.text(); const parsed: unknown = JSON.parse(rawBody); const body = parsed as EtsyWebhookPayload; if (!body.event_type || !body.message.id || !body.message.content) { return c.json({ error: "Invalid payload" }, 400); } const signature = c.req.header("X-Etsy-Signature"); const secret = process.env.ETSY_WEBHOOK_SECRET; if (!signature || !secret) { throw new HTTPException(401, { message: "Invalid signature" }); } const computedHmac = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(computedHmac), Buffer.from(signature))) { throw new HTTPException(401, { message: "Signature mismatch" }); } const etsyMessage: EtsyMessage = { id: body.message.id, senderId: body.message.sender_id, recipientId: body.message.recipient_id, content: body.message.content, timestamp: body.message.timestamp, threadId: body.message.thread_id || body.message.id, }; const pipeline = getPipeline(); const result = await pipeline.runMessage(etsyMessage); if (!result.success) { const flags: ScanFlag[] = [ { type: result.failedGuardrail ?? "unknown", severity: "high", detail: "Flagged by guardrail chain" }, ]; const entry: QuarantineEntry = { messageId: etsyMessage.id, reason: result.failedGuardrail ? `Flagged by guardrail: ${result.failedGuardrail}` : "Flagged by guardrail: unknown", flags, flaggedAt: new Date().toISOString(), status: "quarantined", }; alertSender.sendQuarantineNotification(entry); } return c.json( { received: true, messageId: etsyMessage.id, passed: result.success, quarantined: !result.success, }, 200, ); } catch (err) { if (err instanceof HTTPException) { throw err; } return c.json({ error: "Invalid JSON" }, 400); }});app.get("/health", (c) => { return c.json({ status: "ok", uptime: process.uptime() });});export default app;
Expected output: The webhook server starts. A POST /etsy-webhook with a clean message returns { received: true, passed: true, quarantined: false }. A message with PII or abuse returns { received: true, passed: false, quarantined: true }.
Step 11: Create the dashboard API routes
These Next.js App Router route handlers expose the quarantine store and budget state. Use NextRequest and NextResponse — never bare Request or new Response(JSON.stringify(...)).
Create app/api/quarantine/route.ts:
ts
import { type NextRequest, NextResponse } from "next/server";import { quarantineStore } from "../../../src/alert/sender.js";export function GET(req: NextRequest) { const status = req.nextUrl.searchParams.get("status"); const items = [...quarantineStore.values()]; const filtered = status ? items.filter((e) => e.status === status) : items; return NextResponse.json({ items: filtered, total: filtered.length });}export async function PATCH(req: NextRequest) { const body = (await req.json()) as { messageId?: string; status?: string }; if (!body.messageId) { return NextResponse.json({ error: "messageId is required" }, { status: 400 }); } const entry = quarantineStore.get(body.messageId); if (!entry) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (body.status && ["quarantined", "released", "deleted"].includes(body.status)) { entry.status = body.status as "quarantined" | "released" | "deleted"; } return NextResponse.json({ updated: true, entry });}
Finally, wire up the main entry point in src/index.ts to export every public class and type:
ts
export { GuardrailPipeline } from "./guardrail/pipe.js";export { PiiScanner } from "./pii/scanner.js";export { MistralModeration } from "./moderation/mistral.js";export { ContentClassifier } from "./moderation/classifier.js";export { MessageBudgetTracker } from "./budget/tracker.js";export { AlertSender } from "./alert/sender.js";export type { EtsyMessage } from "./types/message.js";export type { ScanOutcome, QuarantineEntry, ScanFlag, QuarantineStatus } from "./types/guardrail.js";export type { BudgetState, BudgetStateValue } from "./types/budget.js";
Expected output:pnpm typecheck passes with zero errors. src/index.ts exports every runtime class and type so consumers can import them with import { GuardrailPipeline, PiiScanner } from "mistral-etsy-guardrails".
Step 13: Run the tests and verify
This recipe includes a full test suite covering every module: PII scanner, Mistral moderation, content classifier, guardrail config, guardrail pipeline, budget tracker, webhook handler, alert sender, and integration tests. Run the tests with coverage:
terminal
pnpm test
The vitest configuration targets 90% coverage across all four metrics (lines, branches, functions, statements) on runtime source files (src/**/*.ts and app/**/route.ts). UI files like page.tsx and layout.tsx are excluded from coverage requirements — focus your test effort on route handlers, services, and lib modules.
Run type checking and linting separately:
terminal
pnpm typecheckpnpm lint
Expected output:pnpm test prints test results with numFailedTests: 0 and all four coverage metrics at or above 90%. The JSON report is written to vitest-report.json for CI consumption. pnpm typecheck and pnpm lint both exit 0 with no errors.
Next steps
Add Langfuse observability — instrument the guardrail pipeline with OpenTelemetry traces using the langfuse package for production monitoring. The setup already includes Langfuse env vars in .env.example — wire Langfuse into setLogger() to send telemetry.
Deploy on Vercel — the Next.js App Router setup is Vercel-ready. Deploy the webhook and dashboard with vercel deploy and configure your Etsy shop’s webhook URL to point at POST /etsy-webhook.
Extend guardrails — add more guardrail types to the chain: a custom guardrail for URL reputation checking, a sentiment-based stress detector, or an image moderation guardrail for attached product photos.
Persist quarantine data — replace the in-memory Map with a database (SQLite via better-sqlite3 or Postgres via Prisma) so quarantine entries survive server restarts and the dashboard works across instances.