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

Building autonomous AI agents that can actually earn money on freelance marketplaces is more about plumbing than magic. This post walks through a minimal, production‑style pipeline that takes a user prompt, runs it through an LLM chain, turns the output into a concrete gig request, and—if the request is accepted—triggers a micropayment settlement. All code is runnable as‑is; trade‑offs are called out explicitly.


1. Why a Chain, Not a Single Call?

A raw LLM completion is rarely enough to satisfy a gig platform’s API contract. Typical gaps include:

Gap Typical fix in a chain
Structured output (JSON, CSV) LLM → PromptTemplate → OutputParser
Tool use (search, calculation) LLM → Agent with Tools → Observation
Safety / policy check LLM → Moderation → Fallback
Payment reconciliation LLM → Ledger update → Escrow release

Breaking the problem into discrete, testable components makes debugging easier and lets you swap implementations (e.g., replace a local model with a hosted endpoint) without rewriting the whole agent.


2. High‑Level Architecture

+----------------+      +----------------+      +-----------------+
|  User Prompt   | ---> |  LLM Chain     | ---> |  Gig Platform   |
| (REST/Webhook) |      | (LangChain)    |      | (Upwork/Fiverr) |
+----------------+      +----------------+      +-----------------+
        ^                         |                         |
        |                         v                         v
   +----------------+   +----------------+          +-----------------+
   |  Auth & Rate   |   |  Output Parser |          |  Escrow Service |
   |  Limiter       |   +----------------+          +-----------------+
   +----------------+                                 ^
                                                    USDC on Base
Enter fullscreen mode Exit fullscreen mode
  • Ingress – a thin FastAPI endpoint receives a JSON payload ({ "prompt": "...", "max_price": 0.05 }).
  • LLM Chain – built with LangChain; we use a ChatOpenAI model (swap for any compatible endpoint).
  • Output Parser – forces the model to emit a schema that matches the gig platform’s “create job” endpoint (title, description, budget, skills).
  • Gig Platform Adapter – a thin wrapper around the platform’s public API (here we illustrate with Upwork’s OAuth‑2 flow).
  • Escrow/Payment – after the platform returns a job ID, we lock the agreed USDC amount in a simple escrow contract on Base; release occurs when the freelancer marks the job “completed”.

3. Code Walk‑through

3.1 Project Layout

agent/
│   main.py          # FastAPI entrypoint
│   chain.py         # LLM chain definition
│   parser.py        # Pydantic model + parser
│   gig.py           # Upwork adapter (stubbed)
│   escrow.py        # Minimal USDC escrow helper
└   requirements.txt
Enter fullscreen mode Exit fullscreen mode

3.2 requirements.txt

fastapi==0.110.0
uvicorn[standard]==0.29.0
langchain==0.2.0
pydantic==2.7.1
httpx==0.27.0
web3==7.2.0
python-dotenv==1.0.0
Enter fullscreen mode Exit fullscreen mode

3.3 main.py – ingress & orchestration

# main.py
import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from chain import build_chain
from gig import submit_gig
from escrow import lock_funds, release_funds

app = FastAPI(title="LLM‑to‑Gig Agent")

class Request(BaseModel):
    prompt: str
    max_price_usdc: float  # in USDC, e.g. 0.05

# Load once at startup (cold start cost ≈ 200 ms on a Workers instance)
llm_chain = build_chain()

@app.post("/agent")
async def agent(req: Request):
    # 1️⃣ Run the LLM chain
    try:
        raw_output = await llm_chain.ainvoke({"prompt": req.prompt})
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"LLM error: {e}")

    # 2️⃣ Parse into a gig spec (see parser.py)
    from parser import GigSpec, parse_gig_spec
    try:
        spec: GigSpec = parse_gig_spec(raw_output)
    except ValueError as ve:
        raise HTTPException(status_code=400, detail=str(ve))

    # 3️⃣ Enforce price ceiling
    if spec.budget_usdc > req.max_price_usdc:
        raise HTTPException(
            status_code=400,
            detail=f"Budget {spec.budget_usdc} exceeds limit {req.max_price_usdc}",
        )

    # 4️⃣ Submit to gig platform
    try:
        job_id = await submit_gig(spec)
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"Gig platform error: {e}")

    # 5️⃣ Lock funds in escrow (USDC on Base)
    try:
        escrow_tx = lock_funds(job_id, spec.budget_usdc)
    except Exception as e:
        # If escrow fails we still have a live job; we could refund manually.
        raise HTTPException(status_code=500, detail=f"Escrow lock failed: {e}")

    return {
        "job_id": job_id,
        "escrow_tx": escrow_tx,
        "message": "Job posted and funds escrowed. Await freelancer completion."
    }
Enter fullscreen mode Exit fullscreen mode

Trade‑off note:

We keep the LLM chain in a global variable to avoid re‑initializing on each request. This reduces latency (~150 ms) but means any change to the chain requires a redeploy. For true multi‑tenant isolation you’d spin a chain per request, accepting the extra cold‑start cost.

3.4 chain.py – building the LLM pipeline

# chain.py
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser

def build_chain():
    # Replace with your own endpoint or local model (e.g., vLLM)
    llm = ChatOpenAI(
        model_name="gpt-4o-mini",   # cheap, fast; swap for a self‑hosted model
        temperature=0.2,
        openai_api_key=os.getenv("OPENAI_API_KEY"),
    )

    prompt = ChatPromptTemplate.from_messages([
        ("system",
         "You are a helpful assistant that turns a free‑form request into a "
         "structured freelance job posting. Output ONLY valid JSON matching "
         "the GigSpec schema: title, description, budget_usdc (float), "
         "skills (list of strings), duration_hours (int)."),
        ("human", "{prompt}")
    ])

    # The chain: prompt → LLM → string output
    return prompt | llm | StrOutputParser()
Enter fullscreen mode Exit fullscreen mode

Honest trade‑off:

Using a proprietary model (OpenAI) simplifies dev but introduces vendor lock‑in and per‑token cost. If you need strict cost predictability, replace ChatOpenAI with a locally served Llama‑3 via vLLM or TensorRT‑LLM. The chain itself stays unchanged.

3.5 parser.py – enforcing the gig spec

# parser.py
from pydantic import BaseModel, Field, validator
import json
from typing import List

class GigSpec(BaseModel):
    title: str = Field(..., max_length=120)
    description: str = Field(..., max_length=2000)
    budget_usdc: float = Field(..., gt=0)
    skills: List[str] = Field(...)
    duration_hours: int = Field(..., gt=0)

    @validator("budget_usdc")
    def round_budget(cls, v):
        return round(v, 2)   # USDC has 2 decimal places

def parse_gig_spec(raw: str) -> GigSpec:
    # Expect the model to output a JSON object; extra text leads to error.
    try:
        data = json.loads(raw.strip())
    except json.JSONDecodeError as jde:
        raise ValueError(f"Model output not valid JSON: {jde}")
    return GigSpec(**data)
Enter fullscreen mode Exit fullscreen mode

Trade‑off note:

We rely on the model to emit pure JSON. In practice, a small percentage of outputs contain preamble/apology text. Adding a retry loop with a “please output only JSON” instruction mitigates this, but adds latency and extra token consumption.

3.6 gig.py – Upwork adapter (stubbed for brevity)


python
# gig.py
import os
import httpx
from typing import Any

UPWORK_CLIENT_ID = os.getenv("UPWORK_CLIENT_ID")
UPWORK_CLIENT_SECRET = os.getenv("UPWORK_CLIENT_SECRET")
UPWORK_REDIRECT_URI = os.getenv("UPWORK_REDIRECT_URI")  # must be registered

async def _get_access_token() -> str:
    # In a real service you’d refresh/store the token; here we do client‑cred flow.
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "https://www.upwork.com/api/v3/oauth2/token",
            data={
                "grant_type": "client_credentials",
                "client_id": UPWORK_CLIENT_ID,
                "client_secret": UPWORK_CLIENT_SECRET,
            },
        )
        resp.raise_for_status()
        return resp.json()["access_token"]

async def submit_gig(spec: Any) -> str:
Enter fullscreen mode Exit fullscreen mode

Top comments (0)