cult-film-curtis

SKILL.md

Cult Film Curtis - Lucid Agent

Movie-loving connoisseur that provides cult film recommendations for micropayments using TMDB API.

Prerequisites

Environment Variables

export EVM_PRIVATE_KEY="0x..."
export PAYMENTS_RECEIVABLE_ADDRESS="0x..."
export FACILITATOR_URL="https://x402.org/facilitator"
export NETWORK="base"
export TMDB_API_KEY="your_tmdb_api_key"

Implementation

package.json

{
  "name": "cult-film-curtis",
  "type": "module",
  "scripts": {
    "dev": "bun run --hot src/index.ts",
    "start": "bun run src/index.ts"
  },
  "dependencies": {
    "@lucid-agents/core": "latest",
    "@lucid-agents/http": "latest",
    "@lucid-agents/hono": "latest",
    "@lucid-agents/payments": "latest",
    "hono": "^4.0.0",
    "zod": "^4.0.0"
  }
}

src/index.ts

import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { createAgentApp } from '@lucid-agents/hono';
import { payments, paymentsFromEnv } from '@lucid-agents/payments';
import { z } from 'zod';

const TMDB_API_KEY = process.env.TMDB_API_KEY;
if (!TMDB_API_KEY) {
  throw new Error('TMDB_API_KEY environment variable is required');
}
const TMDB_BASE = 'https://api.themoviedb.org/3';

// Cult film keyword IDs from TMDB
const CULT_KEYWORDS = [9717, 12339, 156218]; // cult-film, midnight-movie, b-movie

interface TMDBMovie {
  id: number;
  title: string;
  release_date: string;
  overview: string;
  vote_average: number;
  poster_path: string | null;
  genre_ids: number[];
}

// Cache to avoid rate limits
const cache = new Map<string, { data: any; timestamp: number }>();
const CACHE_TTL = 300_000; // 5 minutes

async function fetchWithCache<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
  const cached = cache.get(key);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }
  const data = await fetcher();
  cache.set(key, { data, timestamp: Date.now() });
  return data;
}

async function fetchCultFilms(page = 1): Promise<TMDBMovie[]> {
  try {
    const keywordId = CULT_KEYWORDS[Math.floor(Math.random() * CULT_KEYWORDS.length)];
    const url = `${TMDB_BASE}/discover/movie?api_key=${TMDB_API_KEY}&with_keywords=${keywordId}&sort_by=vote_count.desc&page=${page}`;
    const res = await fetch(url);
    if (!res.ok) {
      console.error(`TMDB discover failed: ${res.status} ${res.statusText}`);
      return [];
    }
    const data = await res.json();
    return data.results || [];
  } catch (err) {
    console.error('fetchCultFilms error:', err);
    return [];
  }
}

async function fetchMovieDetails(movieId: number): Promise<Record<string, any> | null> {
  try {
    const url = `${TMDB_BASE}/movie/${movieId}?api_key=${TMDB_API_KEY}&append_to_response=credits,keywords`;
    const res = await fetch(url);
    if (!res.ok) {
      console.error(`TMDB movie details failed: ${res.status} ${res.statusText}`);
      return null;
    }
    return await res.json();
  } catch (err) {
    console.error('fetchMovieDetails error:', err);
    return null;
  }
}

async function searchCultFilms(query: string): Promise<TMDBMovie[]> {
  try {
    const url = `${TMDB_BASE}/search/movie?api_key=${TMDB_API_KEY}&query=${encodeURIComponent(query)}`;
    const res = await fetch(url);
    if (!res.ok) {
      console.error(`TMDB search failed: ${res.status} ${res.statusText}`);
      return [];
    }
    const data = await res.json();
    return data.results || [];
  } catch (err) {
    console.error('searchCultFilms error:', err);
    return [];
  }
}

// Create agent with extensions
const agent = await createAgent({
  name: 'cult-film-curtis',
  version: '1.0.0',
  description: 'Cult film recommendations for micropayments',
})
  .use(http())
  .use(payments({ config: paymentsFromEnv() }))
  .build();

const { app, addEntrypoint } = await createAgentApp(agent);

// FREE: Health check
addEntrypoint({
  key: 'health',
  description: 'Health check',
  input: z.object({}),
  price: { amount: 0 },
  handler: async () => ({
    output: {
      status: 'healthy',
      timestamp: new Date().toISOString(),
      tmdbConnected: !!TMDB_API_KEY,
    },
  }),
});

// FREE: Overview of what Curtis offers
addEntrypoint({
  key: 'overview',
  description: 'Free overview of available recommendations',
  input: z.object({}),
  price: { amount: 0 },
  handler: async () => {
    const sample = await fetchWithCache('sample', () => fetchCultFilms(1));
    return {
      output: {
        endpoints: ['recommend', 'details', 'search'],
        dataSource: 'TMDB',
        sampleTitles: sample.slice(0, 5).map((m) => m.title),
        fetchedAt: new Date().toISOString(),
      },
    };
  },
});

// PAID: Get recommendation ($0.01)
addEntrypoint({
  key: 'recommend',
  description: 'Get a cult film recommendation',
  input: z.object({
    mood: z.string().optional().describe('Genre or mood preference'),
    page: z.number().min(1).max(10).optional().describe('Result page (1-10)'),
  }),
  price: { amount: 10000 }, // 0.01 USDC
  handler: async (ctx) => {
    const page = ctx.input.page || Math.ceil(Math.random() * 5);
    const films = await fetchWithCache(`films:${page}`, () => fetchCultFilms(page));

    if (films.length === 0) {
      return { output: { error: 'No films available', fetchedAt: new Date().toISOString() } };
    }

    let selection = films;
    if (ctx.input.mood) {
      const mood = ctx.input.mood.toLowerCase();
      selection = films.filter(
        (f) => f.overview.toLowerCase().includes(mood) || f.title.toLowerCase().includes(mood)
      );
      if (selection.length === 0) selection = films;
    }

    const film = selection[Math.floor(Math.random() * selection.length)];
    return {
      output: {
        id: film.id,
        title: film.title,
        year: film.release_date?.split('-')[0],
        rating: film.vote_average,
        synopsis: film.overview,
        poster: film.poster_path ? `https://image.tmdb.org/t/p/w500${film.poster_path}` : null,
        source: 'tmdb',
        fetchedAt: new Date().toISOString(),
      },
    };
  },
});

// PAID: Detailed film info ($0.005)
addEntrypoint({
  key: 'details',
  description: 'Get detailed information about a specific film',
  input: z.object({
    movieId: z.number().describe('TMDB movie ID'),
  }),
  price: { amount: 5000 }, // 0.005 USDC
  handler: async (ctx) => {
    const details = await fetchWithCache(`details:${ctx.input.movieId}`, () =>
      fetchMovieDetails(ctx.input.movieId)
    );

    if (!details) {
      return { output: { error: 'Movie not found', movieId: ctx.input.movieId, fetchedAt: new Date().toISOString() } };
    }

    return {
      output: {
        id: details.id,
        title: details.title,
        year: details.release_date?.split('-')[0],
        runtime: details.runtime,
        rating: details.vote_average,
        synopsis: details.overview,
        genres: details.genres?.map((g: any) => g.name) || [],
        director: details.credits?.crew?.find((c: any) => c.job === 'Director')?.name,
        cast: details.credits?.cast?.slice(0, 5).map((c: any) => c.name) || [],
        keywords: details.keywords?.keywords?.map((k: any) => k.name) || [],
        poster: details.poster_path ? `https://image.tmdb.org/t/p/w500${details.poster_path}` : null,
        imdbId: details.imdb_id,
        source: 'tmdb',
        fetchedAt: new Date().toISOString(),
      },
    };
  },
});

// PAID: Search films ($0.002)
addEntrypoint({
  key: 'search',
  description: 'Search for cult films by title',
  input: z.object({
    query: z.string().min(2).describe('Search query'),
  }),
  price: { amount: 2000 }, // 0.002 USDC
  handler: async (ctx) => {
    const results = await searchCultFilms(ctx.input.query);
    return {
      output: {
        query: ctx.input.query,
        count: results.length,
        films: results.slice(0, 10).map((f) => ({
          id: f.id,
          title: f.title,
          year: f.release_date?.split('-')[0],
          rating: f.vote_average,
        })),
        fetchedAt: new Date().toISOString(),
      },
    };
  },
});

const port = Number(process.env.PORT ?? 3000);
console.log(`Cult Film Curtis running on port ${port}`);

export default { port, fetch: app.fetch };

Pricing

Endpoint Price Description
/health Free Health check
/overview Free Sample titles, discover endpoints
/recommend 0.01 USDC Random cult film recommendation
/details 0.005 USDC Full movie details with cast/crew
/search 0.002 USDC Search by title

Testing

bun install && bun run dev

# Free endpoints
curl http://localhost:3000/health
curl http://localhost:3000/entrypoints/overview/invoke -X POST -H "Content-Type: application/json" -d '{}'

# Paid endpoint (returns 402 without payment)
curl http://localhost:3000/entrypoints/recommend/invoke -X POST \
  -H "Content-Type: application/json" \
  -d '{"mood": "horror"}'

Deployment

railway login && railway init
railway variables set TMDB_API_KEY="..." EVM_PRIVATE_KEY="0x..." \
  PAYMENTS_RECEIVABLE_ADDRESS="0x..." FACILITATOR_URL="https://x402.org/facilitator" NETWORK="base"
railway up && railway domain
Weekly Installs
5
GitHub Stars
26
First Seen
10 days ago
Installed on
openclaw5
gemini-cli5
github-copilot5
codex5
kimi-cli5
cursor5