[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Verified Marketplace Review System

Build a Verified Marketplace Review System

Add purchase-verified reviews to your marketplace — star ratings, photo uploads, AI moderation for spam and fake reviews, seller replies, and a weighted aggregation score buyers actually trust.

Data & AI#anthropic#claude#llm#ai#tool-use
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 3 skills

Avg quality 91/100·All SAFE
>

anthropic-sdk

v1.0.0

Integrate Claude AI into applications with the Anthropic SDK. Use when a user asks to add Claude to an app, use Claude for text generation, build a chatbot with Claude, use Claude's long context window, implement tool use with Claude, stream Claude responses, use Claude for code generation, document analysis, or reasoning tasks. Covers Messages API, streaming, tool use, vision, system prompts, extended thinking, and batch processing.

93/100 quality
2.86× impact
SAFE
View skill
>

prisma

v1.0.0

You are an expert in Prisma, the TypeScript ORM with a declarative schema, auto-generated type-safe client, migrations, and studio GUI. You help developers model databases with Prisma Schema Language, generate a fully typed client that catches query errors at compile time, run zero-downtime migrations, and integrate with Postgres, MySQL, SQLite, MongoDB, CockroachDB, and PlanetScale.

87/100 quality
1.42× impact
SAFE
View skill
>

s3-storage

v1.0.0

Manages S3-compatible object storage (AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2, Wasabi, Supabase Storage). Use when the user wants to create buckets, upload/download files, set up lifecycle policies, configure CORS, manage presigned URLs, implement multipart uploads, set up replication, handle versioning, configure access policies, or build file management features on top of S3-compatible APIs. Trigger words: s3, minio, r2, object storage, bucket, presigned url, multipart upload, lifecycle policy, s3 cors, storage backend, file storage, blob storage, spaces, backblaze, wasabi.

93/100 quality
1.96× impact
SAFE
View skill
$

Fake reviews kill marketplaces. Unverified ratings are worthless. You need a system that only lets real buyers review, catches spam and fake reviews automatically, and gives sellers a clean way to respond publicly. That's what builds trust.

What You'll Build

  • Purchase-gated reviews: only verified buyers can submit
  • Star rating (1–5) + text + optional photo upload
  • AI moderation: detect spam, fake reviews, and profanity
  • Seller response: public reply visible to all future buyers
  • Weighted aggregation: recency-adjusted score per listing
  • Flagging: other users can flag suspicious reviews

Architecture

Buyer completes order → review prompt unlocked
  → Buyer submits: rating + text + optional photos
  → Photos → S3 upload
  → Claude moderates: spam / fake / profanity check
  → PENDING → auto-approved or queued for human review
  → Score recalculated for listing
  → Seller notified → can reply once

Step 1: Prisma Schema

prisma
model Review {
  id           String        @id @default(cuid())
  listingId    String
  listing      Listing       @relation(fields: [listingId], references: [id])
  orderId      String        @unique  // one review per order
  order        Order         @relation(fields: [orderId], references: [id])
  reviewerId   String
  reviewer     User          @relation("ReviewAuthor", fields: [reviewerId], references: [id])
  rating       Int           // 1-5
  title        String?
  body         String
  photos       ReviewPhoto[]
  status       ReviewStatus  @default(PENDING)
  moderationScore Float?     // 0-1, Claude confidence review is legitimate
  moderationReason String?
  isVerifiedPurchase Boolean @default(true)
  helpfulCount Int           @default(0)
  sellerReply  SellerReply?
  flags        ReviewFlag[]
  createdAt    DateTime      @default(now())
  publishedAt  DateTime?
}

model ReviewPhoto {
  id       String @id @default(cuid())
  reviewId String
  review   Review @relation(fields: [reviewId], references: [id])
  s3Key    String
  url      String // CloudFront URL
}

model SellerReply {
  id        String   @id @default(cuid())
  reviewId  String   @unique
  review    Review   @relation(fields: [reviewId], references: [id])
  sellerId  String
  body      String
  createdAt DateTime @default(now())
}

model ReviewFlag {
  id         String   @id @default(cuid())
  reviewId   String
  review     Review   @relation(fields: [reviewId], references: [id])
  flaggedById String
  reason     String   // spam | fake | inappropriate | other
  createdAt  DateTime @default(now())
  @@unique([reviewId, flaggedById])
}

enum ReviewStatus { PENDING APPROVED REJECTED FLAGGED }

Step 2: Verify Purchase Before Allowing Review

typescript
// lib/reviews.ts
export async function canReview(userId: string, listingId: string): Promise<{
  allowed: boolean;
  orderId?: string;
  reason?: string;
}> {
  // Check completed order exists
  const order = await prisma.order.findFirst({
    where: {
      buyerId: userId,
      listingId,
      status: "COMPLETED",
      review: null, // no review submitted yet
    },
  });

  if (!order) {
    return { allowed: false, reason: "no_verified_purchase" };
  }

  return { allowed: true, orderId: order.id };
}

Step 3: AI Moderation with Claude

typescript
// lib/moderate-review.ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

interface ModerationResult {
  approved: boolean;
  score: number;       // 0-1, probability of legitimate review
  reason?: string;
  flags: string[];     // ["spam", "fake", "profanity", "irrelevant"]
}

export async function moderateReview(review: {
  rating: number;
  title?: string;
  body: string;
  listingTitle: string;
}): Promise<ModerationResult> {
  const prompt = `You are a marketplace review moderator. Analyze this review for quality and legitimacy.

Listing: "${review.listingTitle}"
Rating: ${review.rating}/5
Title: "${review.title ?? "(none)"}"
Review body: "${review.body}"

Check for:
1. SPAM: promotional content, links, unrelated content
2. FAKE: suspiciously generic, copy-paste, no specific details about the product
3. PROFANITY: offensive language
4. IRRELEVANT: review is about shipping/seller but the product is fine (legitimate but misfiled)
5. LEGITIMATE: genuine buyer experience

Return JSON:
{
  "approved": true/false,
  "score": 0.0-1.0,
  "flags": [],
  "reason": "brief explanation if not approved"
}

Be lenient — short genuine reviews should pass. Only reject clear spam/fake content.
Return ONLY JSON.`;

  const message = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 200,
    messages: [{ role: "user", content: prompt }],
  });

  const text = message.content[0].type === "text" ? message.content[0].text : "{}";
  const result = JSON.parse(text);

  return {
    approved: result.approved ?? true,
    score: result.score ?? 0.5,
    reason: result.reason,
    flags: result.flags ?? [],
  };
}

Step 4: Submit Review Endpoint

typescript
// POST /api/reviews
export async function POST(req: Request) {
  const { listingId, rating, title, body, photoKeys } = await req.json();
  const userId = await getSessionUserId(req);

  // Verify purchase
  const { allowed, orderId, reason } = await canReview(userId, listingId);
  if (!allowed) return Response.json({ error: reason }, { status: 403 });

  // Get listing title for moderation context
  const listing = await prisma.listing.findUnique({
    where: { id: listingId },
    select: { title: true },
  });

  // AI moderation
  const moderation = await moderateReview({ rating, title, body, listingTitle: listing!.title });

  // Auto-approve high-confidence legit reviews; queue borderline cases
  const status = moderation.approved && moderation.score > 0.7 ? "APPROVED" : "PENDING";

  const review = await prisma.review.create({
    data: {
      listingId,
      orderId: orderId!,
      reviewerId: userId,
      rating,
      title,
      body,
      status,
      moderationScore: moderation.score,
      moderationReason: moderation.reason,
      publishedAt: status === "APPROVED" ? new Date() : null,
      photos: photoKeys?.length
        ? { create: photoKeys.map((key: string) => ({
            s3Key: key,
            url: getPublicFileUrl(key),
          }))}
        : undefined,
    },
  });

  // Recalculate listing score
  if (status === "APPROVED") await recalculateListingScore(listingId);

  return Response.json({ review, autoApproved: status === "APPROVED" });
}

Step 5: Weighted Score Aggregation

typescript
// Recency-weighted average: recent reviews count more
export async function recalculateListingScore(listingId: string) {
  const reviews = await prisma.review.findMany({
    where: { listingId, status: "APPROVED" },
    select: { rating: true, createdAt: true },
    orderBy: { createdAt: "desc" },
  });

  if (reviews.length === 0) return;

  const now = Date.now();
  let weightedSum = 0;
  let totalWeight = 0;

  reviews.forEach((r, i) => {
    // Exponential decay: newer reviews have higher weight
    const ageMs = now - r.createdAt.getTime();
    const ageDays = ageMs / (1000 * 60 * 60 * 24);
    const weight = Math.exp(-ageDays / 180); // half-life ~180 days
    weightedSum += r.rating * weight;
    totalWeight += weight;
  });

  const score = weightedSum / totalWeight;

  await prisma.listing.update({
    where: { id: listingId },
    data: {
      reviewScore: Math.round(score * 10) / 10,
      reviewCount: reviews.length,
    },
  });
}

Step 6: Seller Reply

typescript
// POST /api/reviews/[id]/reply
export async function POST(req: Request, { params }: { params: { id: string } }) {
  const { body } = await req.json();
  const sellerId = await getSessionUserId(req);

  const review = await prisma.review.findUnique({
    where: { id: params.id },
    include: { listing: true, sellerReply: true },
  });

  if (!review) return Response.json({ error: "not_found" }, { status: 404 });
  if (review.listing.sellerId !== sellerId) return Response.json({ error: "forbidden" }, { status: 403 });
  if (review.sellerReply) return Response.json({ error: "already_replied" }, { status: 409 });

  const reply = await prisma.sellerReply.create({
    data: { reviewId: params.id, sellerId, body },
  });

  return Response.json({ reply });
}

Environment Variables

bash
ANTHROPIC_API_KEY=sk-ant-...
AWS_REGION=us-east-1
S3_BUCKET=my-app-reviews
CLOUDFRONT_DOMAIN=https://cdn.example.com
DATABASE_URL=postgresql://...

Launch Checklist

  • Purchase verification working before unlock
  • Photo upload presigned URL flow
  • Moderation score stored per review
  • Human review queue for PENDING items
  • Weighted score updates on new reviews
  • Seller notification on new review (email)
  • Flag button visible to all users

What's Next

  • Review response templates for sellers
  • Review analytics: common complaints per listing
  • Review incentives: buyer earns points for quality reviews
  • Bulk re-moderation when moderation model improves