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

Autonomous agents that earn money are more than a demo—they require plumbing, reliability checks, and a clear view of the cost model.


Why “prompt‑to‑paycheck” is harder than it looks

A naïve flow looks like this:

  1. Receive a user request (e.g., “write a 500‑word blog post about Rust”).
  2. Feed the request to an LLM.
  3. Return the output and invoice the user.

In practice, each step hides friction:

Step Hidden cost Typical failure mode
Prompt engineering Token waste from over‑specifying or retry loops Output drifts from spec, requiring human review
LLM inference Variable latency (200 ms‑2 s) and price per 1k tokens Rate‑limit bursts cause queue back‑pressure
Platform integration API auth, differing schemas, rate limits 429/403 responses stall the agent
Payment settlement Micropayment fees, reconciliation lag Disputed payouts, fraud flags

If you ignore any of these, the agent will either lose money or get banned from the gig platform. The following sections show a minimal, production‑grade skeleton that addresses each concern explicitly.


System Overview

+----------------+      +----------------+      +----------------+
|  Gig Platform  | <--->|  Adapter Layer | <--->|  Orchestrator  |
| (Upwork/Fiverr)|      | (auth + schema) |      | (state + retry)|
+----------------+      +----------------+      +----------------+
          ^                       ^                       ^
          |                       |                       |
          |   +----------------+  |   +----------------+  |
          +---| Payment Agent  |<-+---| LLM Chain (LangChain)|
              +----------------+       +----------------+
Enter fullscreen mode Exit fullscreen mode
  • Adapter Layer – thin wrappers that translate the gig platform’s REST/GraphQL spec into a uniform Job object (title, description, budget, deadline).
  • Orchestrator – persists job state, handles retries, enforces concurrency limits, and writes audit logs.
  • LLM Chain – a LangChain‑style pipeline that (a) builds a prompt, (b) calls the model, (c) validates output, and (d) optionally asks for a human‑in‑the‑loop check.
  • Payment Agent – escrows funds in USDC on Base via the x402 protocol; releases payment only after the platform marks the job as “completed”.

Each block can be swapped (e.g., replace LangChain with llama.cpp, or use Stripe instead of x402) – the point is to keep the interfaces explicit so you can measure trade‑offs.


1. Adapter Layer – Normalizing Gig Platforms

Below is a Python adapter for a hypothetical “GigHub” API that mimics Upwork’s job posting endpoint. Real platforms differ only in field names and auth mechanisms; the adapter isolates those details.

# gig_adapter.py
import requests
from dataclasses import dataclass
from typing import Optional

@dataclass
class Job:
    id: str
    title: str
    description: str
    budget_usd: float
    deadline: Optional[str]  # ISO8601 or None
    platform: str            # e.g., "upwork", "fiverr"

class GigHubAdapter:
    BASE_URL = "https://api.gighub.example.com/v1"

    def __init__(self, api_key: str):
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})

    def fetch_open_jobs(self, limit: int = 50) -> list[Job]:
        resp = self.session.get(
            f"{self.BASE_URL}/jobs/open",
            params={"limit": limit},
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()
        jobs = []
        for item in data["jobs"]:
            jobs.append(
                Job(
                    id=item["job_id"],
                    title=item["title"],
                    description=item["description"],
                    budget_usd=float(item["budget"]["amount"]),
                    deadline=item.get("deadline"),
                    platform="gighub",
                )
            )
        return jobs

    def submit_result(self, job_id: str, result: str) -> bool:
        payload = {"job_id": job_id, "deliverable": result}
        resp = self.session.post(
            f"{self.BASE_URL}/jobs/{job_id}/submit",
            json=payload,
            timeout=10,
        )
        if resp.status_code == 200:
            return True
        # 429 → retry later, 4xx → permanent failure
        return resp.status_code < 500
Enter fullscreen mode Exit fullscreen mode

Trade‑off:

The adapter is deliberately synchronous for clarity. In production you’d wrap it in an async worker pool (e.g., anyio or trio) to avoid blocking the orchestrator while waiting on platform latency.


2. Orchestrator – State, Retries, and Concurrency

A simple SQLite‑backed state machine prevents duplicate work and gives you a place to store retry counters.

# orchestrator.py
import sqlite3
import time
from dataclasses import asdict
from typing import List
from gig_adapter import Job, GigHubAdapter

DB_PATH = "agent_state.db"

def init_db():
    with sqlite3.connect(DB_PATH) as con:
        con.execute(
            """
            CREATE TABLE IF NOT EXISTS jobs (
                id TEXT PRIMARY KEY,
                platform TEXT,
                title TEXT,
                description TEXT,
                budget REAL,
                deadline TEXT,
                status TEXT,          -- NEW, IN_PROGRESS, DONE, FAILED
                attempts INTEGER,
                last_error TEXT
            )
            """
        )

def enqueue_new_jobs(adapter: GigHubAdapter, limit: int = 20):
    init_db()
    fresh: List[Job] = adapter.fetch_open_jobs(limit=limit)
    with sqlite3.connect(DB_PATH) as con:
        for job in fresh:
            con.execute(
                """
                INSERT OR IGNORE INTO jobs
                (id, platform, title, description, budget, deadline, status, attempts)
                VALUES (?, ?, ?, ?, ?, ?, 'NEW', 0)
                """,
                (job.id, job.platform, job.title, job.description,
                 job.budget_usd, job.deadline),
            )

def claim_job() -> Optional[Job]:
    with sqlite3.connect(DB_PATH) as con:
        cur = con.execute(
            """
            SELECT id, platform, title, description, budget, deadline
            FROM jobs
            WHERE status = 'NEW'
            ORDER BY ROWID ASC
            LIMIT 1 FOR UPDATE SKIP LOCKED
            """
        )
        row = cur.fetchone()
        if not row:
            return None
        job_id, platform, title, desc, budget, deadline = row
        con.execute(
            "UPDATE jobs SET status='IN_PROGRESS', attempts=attempts+1 WHERE id=?",
            (job_id,),
        )
        return Job(job_id, title, desc, budget, deadline, platform)

def mark_done(job_id: str):
    with sqlite3.connect(DB_PATH) as con:
        con.execute(
            "UPDATE jobs SET status='DONE' WHERE id=?",
            (job_id,),
        )

def mark_failed(job_id: str, error: str):
    with sqlite3.connect(DB_PATH) as con:
        con.execute(
            """
            UPDATE jobs
            SET status='FAILED', last_error=?
            WHERE id=?
            """,
            (error, job_id),
        )
Enter fullscreen mode Exit fullscreen mode

Trade‑off:

Using SELECT … FOR UPDATE SKIP LOCKED lets multiple worker processes safely claim jobs without a external queue (e.g., Redis). If you need horizontal scaling beyond a single machine, replace this with a true job queue (RabbitMQ, SQS) and accept the extra operational overhead.


3. LLM Chain – Prompting, Validation, and Cost Guardrails

We’ll use LangChain’s LLMChain with a simple OpenAI‑compatible endpoint. The chain includes a self‑check step that asks the model to verify word count and tone before returning the final answer.


python
# llm_chain.py
from langchain import LLMChain, PromptTemplate
from langchain.llms import OpenAI
import os
import re

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
llm = OpenAI(temperature=0.7, max_tokens=800, openai_api_key=OPENAI_API_KEY)

# 1️⃣ Generation prompt
gen_tmpl = PromptTemplate(
    input_variables=["title", "description"],
    template=(
        "You are a professional freelance writer. "
        "Write a {title} piece that satisfies the following brief:\n\n"
        "{description}\n\n"
        "Constraints: 450‑550 words, neutral tone, no markdown."
    ),
)
gen_chain = LLMChain(llm=llm, prompt=gen_tmpl)

# 2️⃣ Self‑check prompt (asks the model to verify its own output)
check_tmpl = PromptTemplate(
    input_variables=["draft"],
    template=(
        "Review the following text
Enter fullscreen mode Exit fullscreen mode

Top comments (0)