DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms

From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms

For developers building autonomous AI agents that actually earn money.


1. Why a “prompt‑to‑paycheck” pipeline matters

Most demos stop at a clever chat completion. To turn that into a billable service you need three layers that work together:

Layer Responsibility Typical failure points
Prompt/Chain Turns a user request into a deterministic sequence of LLM calls, tool uses, and post‑processing. Hallucination, token blow‑up, uncontrolled recursion.
Execution Runtime Hosts the chain, manages state, retries, and exposes a clean HTTP/JSON‑RPC endpoint. Cold start latency, scaling limits, secret leakage.
Payment & Metering Records each successful call, charges the caller in USDC (or another stablecoin) via a smart‑contract escrow, and optionally refunds on failure. Gas price volatility, replay attacks, disputable outcomes.

If any layer is weak, the whole pipeline either loses money (over‑charging or under‑charging) or trust (bad outputs, missed SLAs). The following sections show a minimal, production‑ish implementation that keeps each concern isolated while staying easy to iterate on.


2. The LLM chain: deterministic, observable, and cheap

We’ll use LangChain (v0.2+) because it gives us composable Runnable objects, built‑in token counting, and easy swapping of back‑ends (OpenAI, Anthropic, local Llama.cpp). The example chain does three things:

  1. Extract intent – a tiny classifier that maps a free‑form gig request to a known skill (e.g., “write SEO blog post”).
  2. Run a skill‑specific sub‑chain – a prompt‑filled LLM call plus optional tool use (e.g., web search).
  3. Validate output – a lightweight regex/JSON schema check; if it fails we retry up to N times or fallback to a human‑in‑the‑loop queue.
# file: agent_chain.py
from langchain_core.runnables import RunnableSequence, RunnableLambda
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import JsonOutputParser
import json, re, os

LLM = ChatOpenAI(model="gpt-4o-mini", temperature=0.2, api_key=os.getenv("OPENAI_KEY"))

# 1️⃣ Intent classifier – returns {"skill": "seo_blog", "confidence": 0.93}
INTENT_PROMPT = ChatPromptTemplate.from_messages([
    ("system", "You are a gig‑router. Classify the user request into one of: "
               "[seo_blog, social_copy, code_review, data_summarize]. "
               "Return JSON with keys skill and confidence (0‑1)."),
    ("human", "{request}")
])
intent_chain = INTENT_PROMPT | LLM | JsonOutputParser()

# 2️⃣ Skill‑specific sub‑chains (example: SEO blog)
SEO_PROMPT = ChatPromptTemplate.from_messages([
    ("system", "Write a 600‑word SEO‑optimized blog post about {topic}. "
               "Include H2 headings, bullet points, and a meta description ≤160 chars."),
    ("human", "{topic}")
])
SEO_CHAIN = SEO_PROMPT | LLM | (lambda x: x.content)  # raw text output

# 3️⃣ Validator – checks length and presence of meta description
def validate_seo_blog(text: str) -> dict:
    if not (500 <= len(text) <= 800):
        raise ValueError("Length out of bounds")
    if not re.search(r"(?i)meta description:", text):
        raise ValueError("Missing meta description")
    return {"output": text, "status": "ok"}

SEO_VALIDATOR = RunnableLambda(validate_seo_blog)

# Assemble the full chain with retry logic
def make_skill_chain(skill: str):
    if skill == "seo_blog":
        return intent_chain | (lambda d: SEO_CHAIN.invoke({"topic": d["request"]})) | SEO_VALIDATOR
    # add other skills similarly …
    raise NotImplementedError(skill)

# Entry point used by the runtime
def run_chain(request: str) -> dict:
    # First classify
    intent = intent_chain.invoke({"request": request})
    skill = intent.get("skill")
    if not skill:
        return {"error": "Unable to classify request", "status": "fail"}
    try:
        result = make_skill_chain(skill).invoke({"request": request})
        return {**intent, **result, "status": "ok"}
    except Exception as e:
        # simple retry – could be swapped for exponential backoff + dead‑letter queue
        return {"error": str(e), "status": "fail", "intent": intent}
Enter fullscreen mode Exit fullscreen mode

Trade‑offs in the chain

Decision Why we chose it Cost / Risk
Mini model (gpt-4o-mini) for classification Cheap, low latency (~150 ms), sufficient for a 4‑class problem. Might mis‑classify ambiguous prompts → fallback to human review.
Full‑size model for skill execution (you could swap to a cheaper model per skill) Quality matters for deliverable content; we keep the option to downgrade later. Higher per‑token cost (~$0.006/1k tokens).
Deterministic validator + retry Guarantees a minimum SLA (length, required fields) before we charge. Adds latency (extra LLM call on failure) and complexity; we cap retries to 2 to avoid runaway loops.
JSON‑only interfaces Easy to unit test, monitor, and plug into any HTTP gateway. Slight overhead vs. raw strings, but negligible compared to LLM latency.

3. Runtime: exposing the chain as a billable micro‑service

We deploy the chain behind a Cloudflare Workers script (you could also use AWS Lambda, Fly.io, or a self‑hosted Kubernetes pod). The worker does three things:

  1. Authenticate the caller via a signed JWT (issued by your platform’s billing system).
  2. Meter the request – increment a usage counter in a durable object or KV store.
  3. Execute the chain, returning the result or an error payload with the appropriate HTTP status.
// file: worker.js
import { Chain } from "./agent_chain.js"; // assuming we transpile/pyodide‑wrap the Python logic
import { verifyJwt } from "./auth.js";

const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const PRICE_MAP = {
  seo_blog: 0.05,   // $0.05 per successful call
  social_copy: 0.02,
  code_review: 0.08,
  data_summarize: 0.04,
};

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  if (url.pathname !== "/run") return new Response("Not Found", { status: 404 });

  // 1️⃣ Auth
  const auth = request.headers.get("Authorization") || "";
  const match = auth.match(/^Bearer\s+(.+)$/);
  if (!match) return unauthorized();
  const jwt = match[1];
  let payload;
  try { payload = await verifyJwt(jwt, /* publicKey */); }
  catch (e) { return unauthorized(); }

  const userId = payload.sub; // e.g., your platform's internal user id
  // 2️⃣ Parse body
  let body;
  try { body = await request.json(); }
  catch { return badRequest("Invalid JSON"); }
  const { prompt } = body;
  if (typeof prompt !== "string") return badRequest("Missing 'prompt' field");

  // 3️⃣ Meter + price lookup (we defer actual on‑chain charge to a webhook)
  const intent = await Chain.classify(prompt); // reuse the intent classifier from Python
  const skill = intent.skill;
  const priceUSDC = PRICE_MAP[skill] ?? 0;
  if (priceUSDC === 0) return new Response("Unsupported skill", { status: 400 });

  // Increment usage counter (Durable Object or KV)
  await incrementUsage(userId, skill, 1); // pseudo‑function

  // 4️⃣ Run chain
  let result;
  try { result = await Chain.run(prompt); }
  catch (err) {
    // On failure we still count the attempt (you may choose to refund)
    await logFailure(userId, skill, err.message);
    return new Response(JSON.stringify({ error: err.message, status: "fail" }), {
      status: 500,
      headers: { "Content-Type": "application/json" }
    });
  }

  // 5️⃣ Successful response – include meta for billing webhook
  return new Response(JSON.stringify({
    ...result,
    billing: { skill, priceUSDC, userId, timestamp: Date.now() }
  }), {
    status: 200,
    headers: { "Content-Type": "application/json" }
  });
}

/* Helper responders */
function unauthorized() { return new Response("Unauthorized", { status: 401 }); }
function badRequest(msg) { return new Response(msg, { status: 400, headers: {"Content-Type":"text/plain"}}); }

/* Stubs – replace with your actual storage */
async function incrementUsage(userId, skill, cnt) { /* KV increment */ }
async function logFailure(userId, skill, msg) { /* log to R2 or external service */ }
Enter fullscreen mode Exit fullscreen mode

Observability & reliability notes

  • Durable Objects give us per‑user

Top comments (0)