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
-
Ingress – a thin FastAPI endpoint receives a JSON payload (
{ "prompt": "...", "max_price": 0.05 }). -
LLM Chain – built with LangChain; we use a
ChatOpenAImodel (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
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
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."
}
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()
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)
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:
Top comments (0)