From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms
Building autonomous agents that can find work, craft proposals, and get paid isn’t science‑fiction—it’s a matter of gluing together a few well‑understood pieces. Below is a step‑by‑step walkthrough of a minimal, production‑ready chain that takes a natural‑language prompt, runs it through an LLM, uses the output to call a gig‑platform API, and settles the transaction with an x402 micropayment. The goal is to show the moving parts, the realistic costs, and the trade‑offs you’ll hit when you try to run this at scale.
1. High‑level Architecture
+----------------+ +----------------+ +-----------------+
| User Prompt | ---> | LLM Chain | ---> | Gig‑Platform API|
+----------------+ +----------------+ +-----------------+
| | |
v v v
(structured) (tool call) (job proposal /
JSON spec) … invoice)
| | |
+-----------+-------------+-----------------------+
|
v
+-----------------+
| x402 Payment |
+-----------------+
- Prompt – free‑form text from a developer or a higher‑level orchestrator (e.g., “Find a React gig paying >$30/hr and write a 150‑word proposal”).
- LLM Chain – a LangChain‑style pipeline that extracts constraints, calls a retrieval tool (optional), and formats a JSON payload for the platform.
- Gig‑Platform API – we’ll use a mock REST endpoint that mimics the core actions of Upwork/Fiverr: search jobs, create a proposal, and return an invoice ID. In a real integration you’d replace the base URL and auth headers with the platform’s OAuth flow.
-
x402 Payment – a lightweight HTTP‑based micropayment protocol. After the platform returns an invoice, the agent signs a USDC transaction on Base and includes the proof in the
X-402-Paymentheader. The platform validates it and marks the job as “paid”.
The chain is deliberately synchronous for clarity; in production you’d push each stage onto a job queue (e.g., AWS SQS) to absorb spikes and retry failures.
2. Prompt Design – From Free Text to Structured Intent
The LLM does not need to be a reasoning beast; it just needs to obey a strict output schema. We use a few‑shot prompt that forces JSON.
# prompt_template.txt
"""
You are a job‑agent assistant. Given a natural‑language request, return a JSON object
with the following keys:
- action: one of ["search", "propose"]
- query: free‑text search string (for action=search)
- max_results: integer (default 10)
- proposal: object with fields title, cover_letter (string), budget_usd (float)
(only required for action=propose)
If the request is ambiguous, ask for clarification by returning:
{ "action": "clarify", "message": "<your question>" }
Examples:
Request: "Find React gigs paying over $30/hr"
Response:
{
"action": "search",
"query": "React developer",
"max_results": 20,
"filters": { "min_hourly_rate": 30 }
}
Request: "Write a proposal for job_id 12345"
Response:
{
"action": "propose",
"job_id": "12345",
"proposal": {
"title": "Experienced React Frontend Engineer",
"cover_letter": "I have 5 years of building scalable SPAs...",
"budget_usd": 1500
}
}
"""
Why JSON?
Parsing free‑form LLM output is fragile and invites injection attacks. By constraining the model to a known schema we can validate with pydantic or jsonschema before proceeding, which eliminates a large class of runtime bugs.
3. LLM Chain Implementation (Python)
We’ll use LangChain for its composable LLMChain and OpenAI (gpt-4o-mini for cost‑effectiveness) as the LLM. The chain consists of three steps:
- Prompt formatting – inject the user request into the template.
- LLM call – generate JSON.
- Output parsing & validation – turn the string into a pydantic model; raise on failure.
# agent_chain.py
import json
from typing import Literal, Optional
from pydantic import BaseModel, Field, ValidationError
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain_openai import ChatOpenAI
# ----- 1. Pydantic models matching the schema -----
class SearchRequest(BaseModel):
action: Literal["search"]
query: str
max_results: Optional[int] = 10
filters: Optional[dict] = None
class ProposeRequest(BaseModel):
action: Literal["propose"]
job_id: str
proposal: dict = Field(
...,
description="Must contain title, cover_letter, budget_usd"
)
class ClarifyRequest(BaseModel):
action: Literal["clarify"]
message: str
AgentRequest = SearchRequest | ProposeRequest | ClarifyRequest
# ----- 2. Prompt template -----
template = open("prompt_template.txt").read()
prompt = PromptTemplate(
input_variables=["user_request"],
template=template,
)
# ----- 3. LLM (choose a cheap, fast model) -----
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.0, # deterministic output helps validation
max_tokens=500,
)
# ----- 4. Chain -----
chain = LLMChain(llm=llm, prompt=prompt)
def run_chain(user_request: str) -> AgentRequest:
"""Execute the LLM chain and return a validated request object."""
raw = chain.run(user_request=user_request)
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError(f"LLM did not return valid JSON: {raw}") from e
# Discriminate by action field
action = data.get("action")
if action == "search":
return SearchRequest(**data)
if action == "propose":
return ProposeRequest(**data)
if action == "clarify":
return ClarifyRequest(**data)
raise ValueError(f"Unknown action '{action}' in LLM output")
Trade‑offs visible here
| Aspect | Choice | Reason | Cost / Risk |
|---|---|---|---|
| Model | gpt-4o-mini |
~ $0.00015 per 1k tokens, low latency (~300 ms) | Slightly less reasoning power than GPT‑4; may need clarification loops for vague prompts. |
| Temperature | 0.0 |
Guarantees repeatable JSON → easier validation | Removes creativity; if the model needs to guess missing fields it will fail rather than hallucinate. |
| Validation | Pydantic | Guarantees schema conformity before any side‑effects | Adds a tiny CPU overhead; malformed JSON triggers a retry or fallback to human review. |
4. Gig‑Platform Integration
Below is a minimal client for a hypothetical platform that exposes three endpoints:
-
GET /jobs?query=...&max_results=...&filters=...→ list of jobs -
POST /proposals→ submit a proposal (requiresjob_id,title,cover_letter,budget_usd) -
POST /payments→ receive an x402‑style invoice (returnspayment_requestwith amount, asset, and destination)
Replace BASE_URL and the auth header with the real platform’s OAuth2 bearer token.
python
# platform_client.py
import requests
from typing import Any, Dict
BASE_URL = "https://api.example-gig.com/v1"
HEADERS = {
"Authorization": "Bearer <YOUR_PLATFORM_TOKEN>",
"Content-Type": "application/json",
}
def search_jobs(params: Dict[str, Any]) -> Dict[str, Any]:
resp = requests.get(f"{BASE_URL}/jobs", headers=HEADERS, params=params, timeout=10)
resp.raise_for_status()
return resp.json()
def submit_proposal(payload: Dict[str, Any]) -> Dict[str, Any]:
resp = requests.post(f"{BASE_URL}/proposals", headers=HEADERS, json=payload, timeout=10)
resp.raise_for_status()
return resp.json()
def create_invoice(amount_usdc: float) -> Dict[str, Any]:
"""
Ask the platform for an x402 payment request.
The platform returns:
{
"amount": "0.05",
"asset": "USDC",
"destination": "0xAbc...",
"expires_at": 1735689600,
"id": "inv_123"
}
"""
payload = {"amount": amount_usdc, "asset": "USDC"}
resp = requests.post(f"{BASE_URL}/payments", headers=HEADERS, json=payload, timeout=10)
resp.raise_for_status
Top comments (0)