From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms
Building autonomous AI agents that can accept, execute, and get paid for real‑world tasks isn’t magic—it’s plumbing. Below is a step‑by‑step walkthrough of how to connect an LLM‑driven chain to a gig‑economy marketplace, the gotchas you’ll hit along the way, and code you can run today.
1. The High‑Level Flow
- Trigger – A client posts a job (e.g., “Write a 500‑word blog intro about renewable energy”).
- Ingestion – Your agent pulls the job description via the platform’s API.
- Planning – The LLM decides which tools (research, drafting, SEO check) are needed and in what order.
- Execution – Each tool runs, returns structured data, and feeds back into the LLM for the next step.
- Submission – The final artifact is posted back to the platform as a deliverable.
- Payment – Upon completion, the platform releases funds (or you invoke a micro‑payment hook like x402).
The core difficulty is stateful tool orchestration while staying within latency and cost budgets that make the agent economically viable.
2. Choosing the Stack
| Concern | Recommended Choice | Why |
|---|---|---|
| LLM backbone | gpt‑4‑turbo (or a self‑hosted Mixtral‑8x7B) | Good trade‑off between reasoning depth and token cost for multi‑step chains. |
| Orchestration | LangChain Expression Language (LCEL) + RunnableSequence | Declarative, easy to insert custom tools, and supports async streaming. |
| Tool abstraction | BaseTool subclasses (Python) | Uniform interface for sync/async actions, automatic input validation. |
| Gig‑platform API | REST + Webhooks (most platforms expose both) | REST for pulling jobs, webhooks for push‑based status updates (reduces polling). |
| Payment / settlement | x402 (USDC on Base) or platform escrow | Enables per‑call micropayments without trusting a third‑party custodian. |
| Observability | LangSmith + OpenTelemetry | Trace token usage, latency, and tool failures in real time. |
3. Skeleton Agent Code
Below is a minimal, production‑ready example that shows how to wire everything together. Replace placeholders (<…>) with your actual credentials.
# agent.py
import os
import asyncio
from typing import List, Dict
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnableSequence, RunnableLambda
from langchain_core.tools import BaseTool
from langchain_core.messages import HumanMessage, SystemMessage
import httpx
# ----------------------------------------------------------------------
# 1️⃣ LLM
# ----------------------------------------------------------------------
llm = ChatOpenAI(
model="gpt-4-turbo-preview",
temperature=0.2,
api_key=os.getenv("OPENAI_API_KEY"),
)
# ----------------------------------------------------------------------
# 2️⃣ Gig‑platform connector (example: generic REST job board)
# ----------------------------------------------------------------------
GIG_API_BASE = os.getenv("GIG_API_BASE")
GIG_API_KEY = os.getenv("GIG_API_KEY")
async def fetch_open_jobs() -> List[Dict]:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{GIG_API_BASE}/jobs?status=open",
headers={"Authorization": f"Bearer {GIG_API_KEY}"},
)
resp.raise_for_status()
return resp.json()["jobs"]
async def submit_deliverable(job_id: str, artifact: str) -> None:
async with httpx.AsyncClient() as client:
await client.post(
f"{GIG_API_BASE}/jobs/{job_id}/deliver",
json={"content": artifact},
headers={"Authorization": f"Bearer {GIG_API_KEY}"},
)
# ----------------------------------------------------------------------
# 3️⃣ Tools – research, draft, SEO check
# ----------------------------------------------------------------------
class WebSearchTool(BaseTool):
name: str = "web_search"
description: str = "Fetch top‑N search results for a query."
def _run(self, query: str, *, top_k: int = 5) -> List[Dict]:
# Placeholder: replace with your preferred search API (SerpAPI, Bing, etc.)
raise NotImplementedError
async def _arun(self, query: str, *, top_k: int = 5) -> List[Dict]:
# Async wrapper – implement real call here
return []
class DraftTool(BaseTool):
name: str = "draft"
description: str = "Produce a coherent paragraph given an outline."
def _run(self, outline: str) -> str:
prompt = [
SystemMessage(content="You are a concise copywriter."),
HumanMessage(content=f"Expand the following outline into a polished paragraph:\n{outline}"),
]
return llm.invoke(prompt).content
async def _arun(self, outline: str) -> str:
return self._run(outline)
class SEOCoreTool(BaseTool):
name: str = "seo_check"
description: str = "Return a readability score and keyword density."
def _run(self, text: str) -> Dict:
# Very lightweight placeholder – replace with real library (textstat, etc.)
return {"readability": 0.0, "keyword_density": {}}
async def _arun(self, text: str) -> Dict:
return self._run(text)
# ----------------------------------------------------------------------
# 4️⃣ Build the chain: plan → tool execution → refine → submit
# ----------------------------------------------------------------------
def planner(state: Dict) -> Dict:
"""Ask the LLM to decide which tool to run next."""
messages = [
SystemMessage(content=(
"You are an agent that decides the next action. "
"Output a JSON with keys: 'tool' (one of 'web_search','draft','seo_check') "
"and 'input' (the argument for that tool)."
)),
HumanMessage(content=str(state)),
]
decision = llm.invoke(messages).content
# In practice, parse JSON safely; here we assume well‑formed output.
import json
return json.loads(decision)
def tool_executor(state: Dict) -> Dict:
"""Run the selected tool and merge its output back into state."""
tool_name = state["tool"]
tool_input = state["input"]
tool_map = {
"web_search": WebSearchTool(),
"draft": DraftTool(),
"seo_check": SEOCoreTool(),
}
tool = tool_map[tool_name]
# Use async version if we are inside an async context; sync for simplicity here.
result = tool._run(**tool_input) if isinstance(tool, BaseTool) else None
return {**state, "tool_output": result}
def refine(state: Dict) -> Dict:
"""Let the LLM incorporate tool output and decide if we are done."""
messages = [
SystemMessage(content=(
"You are the reasoning core. Given the current state, produce either: "
"{'action': 'continue', 'next_tool': ..., 'next_input': ...} "
"or {'action': 'finish', 'artifact': <final text>}."
)),
HumanMessage(content=str(state)),
]
decision = llm.invoke(messages).content
import json
return json.loads(decision)
# Assemble a RunnableSequence that loops until finish.
async def run_agent(job: Dict) -> None:
state = {"job_description": job["description"]}
while True:
plan = planner(state)
state = {**state, **plan}
state = tool_executor(state)
decision = refine(state)
if decision.get("action") == "finish":
artifact = decision["artifact"]
await submit_deliverable(job["id"], artifact)
break
# otherwise loop with next tool/input from decision
state = {**state, **decision}
# ----------------------------------------------------------------------
# 5️⃣ Entry point – poll for jobs (or use webhook)
# ----------------------------------------------------------------------
async def main():
while True:
jobs = await fetch_open_jobs()
for job in jobs:
# Simple deduplication: skip if we already processed this ID.
# In production, persist processed IDs to a DB or Redis.
asyncio.create_task(run_agent(job))
await asyncio.sleep(30) # poll interval; adjust per platform rate limits
if __name__ == "__main__":
asyncio.run(main())
What the snippet does
- Planner – asks the LLM to propose the next tool and its arguments.
- Tool executor – runs the chosen tool (search, draft, SEO) and stores the raw output.
- Refiner – lets the LLM decide whether the output is sufficient or if another tool is needed, ultimately emitting a final artifact.
-
Loop – repeats until the LLM signals
finish.
The loop is deliberately stateless per iteration; all needed context lives in the state dict that gets threaded through the chain. This makes it easy to persist checkpoints (e.g., write state to a DB after each iteration) and resume after a crash.
4. Honest Trade‑offs
| Area | Benefit | Cost / Gotcha |
|---|---|---|
| LLM‑driven planning | Flexible, can adapt to unseen job types without hard‑coding flow. | Each planning step adds ~150‑300 tokens; latency can exceed 2 s on slower models, making real‑time response feel sluggish. |
| Tool abstraction | Swapping a search API or swapping in a local model is a one‑line change. | Every tool call incurs an extra network hop; if you chain 4‑5 tools you may hit platform rate limits quickly. |
| Async polling | Simple to implement; works with platforms that lack push webhooks. | Polling wastes compute and may miss timely |
Top comments (0)