From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms
Building autonomous agents that can actually earn money requires more than a clever prompt. You need reliable plumbing between the language model, the gig‑platform APIs, and a payment settlement layer. Below is a walk‑through of the pieces that work today, the code that glues them together, and the trade‑offs you’ll hit when you move from demo to production.
1. Why a Chain, Not Just a Prompt?
A single LLM call can draft a proposal, but gig platforms expect a sequence of actions:
- Discover a task that matches the agent’s skill set.
- Parse the task description to extract requirements (budget, deadline, tech stack).
- Generate a tailored bid or solution artifact.
- Submit the bid via the platform’s API.
- Handle the platform’s response (acceptance, rejection, request for clarification).
Each step can be modeled as a tool in an LLM chain. The chain gives you deterministic error handling, retries, and the ability to swap models without rewriting platform‑specific logic.
2. High‑Level Architecture
+----------------+ +----------------+ +----------------+
| Scheduler | ---> | LLM Chain | ---> | Gig‑Platform |
| (cron / trigger)| | (prompt + tools)| | API (REST/GraphQL)|
+----------------+ +----------------+ +----------------+
^ | |
| v v
+----------------+ +----------------+ +----------------+
| State Store | <---> | Payment Ledger| <---> | Escrow / USDC |
+----------------+ +----------------+ +----------------+
- Scheduler – a lightweight job (e.g., a Cloudflare Worker cron) that kicks off the chain every few minutes.
- LLM Chain – built with LangChain‑like primitives; each step is a Python function that returns a structured payload.
- Gig‑Platform API – we’ll show Upwork’s GraphQL endpoint as an example; the pattern applies to Fiverr, Freelancer, etc.
- State Store – Redis or SQLite to keep track of which tasks have been seen, bids sent, and outcomes.
- Payment Ledger – records the USDC amount earned per successful bid; the x402 spec lets you attach a micro‑payment to each HTTP response.
3. Prompt Engineering & Tool Definitions
We keep prompts short, version‑controlled, and testable. Below is a reusable template for the “Parse Task” step.
# prompts/parse_task.txt
You are a careful analyst. Given the following gig description, extract:
- title (string)
- required_skills (list of strings)
- budget_min (float, USD)
- budget_max (float, USD)
- deadline (ISO‑8601 string or null)
Return ONLY a JSON object with those keys. If a field cannot be determined, set it to null.
---
{{task_description}}
The corresponding tool function:
import json
import re
from typing import Any, Dict, Optional
def parse_task_tool(task_description: str) -> Dict[str, Any]:
"""
Sends the description to the LLM and forces a JSON output.
Falls back to a regex‑based heuristic if the model returns malformed JSON.
"""
prompt = open("prompts/parse_task.txt").read().replace("{{task_description}}", task_description)
raw = call_llm(prompt, temperature=0.0, max_tokens=200) # see §4 for call_llm
# Attempt to isolate JSON
json_match = re.search(r"\{.*\}", raw, re.DOTALL)
if not json_match:
raise ValueError("LLM did not return JSON-like output")
try:
data = json.loads(json_match.group(0))
except json.JSONDecodeError:
# Very lightweight fallback: look for key=value patterns
data = {}
for key in ["title", "required_skills", "budget_min", "budget_max", "deadline"]:
m = re.search(rf"{key}\s*[:=]\s*['\"]?([^'\"\n,]+)", raw, re.I)
if m:
val = m.group(1).strip()
if key == "required_skills":
data[key] = [s.strip() for s in val.split(",")]
elif key in ("budget_min", "budget_max"):
try:
data[key] = float(val)
except ValueError:
data[key] = None
elif key == "deadline":
data[key] = val if val.lower() != "null" else None
else:
data[key] = val
else:
data[key] = None
# Normalise types
data.setdefault("required_skills", [])
data.setdefault("budget_min", None)
data.setdefault("budget_max", None)
data.setdefault("deadline", None)
return data
Trade‑off: Using a low‑temperature model improves JSON reliability but can make the output brittle if the prompt drifts. Keeping a fallback parser prevents total failure at the cost of slightly less nuanced extraction.
4. Calling the LLM – Minimal Wrapper
You can swap the provider without touching the rest of the chain.
import os
import requests
def call_llm(prompt: str, temperature: float = 0.2, max_tokens: int = 500) -> str:
"""
Simple wrapper around OpenAI's chat completions API.
Replace the endpoint and headers for other providers (e.g., Anthropic, local vLLM).
"""
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("OPENAI_API_KEY not set")
url = "https://api.openai.com/v1/chat/completions"
payload = {
"model": "gpt-4o-mini", # cheap, decent reasoning
"messages": [{"role": "user", "content": prompt}],
"temperature": temperature,
"max_tokens": max_tokens,
}
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
resp = requests.post(url, json=payload, headers=headers, timeout=15)
resp.raise_for_status()
data = resp.json()
return data["choices"][0]["message"]["content"].strip()
Trade‑off: gpt-4o-mini costs roughly $0.00015 per 1k tokens, which keeps per‑call spend under a cent for most prompts. If latency is critical, a local quantized model (e.g., Mistral‑7B‑instruct) can cut network latency but adds maintenance overhead and often lower reasoning quality.
5. Interacting with a Gig Platform (Upwork Example)
Upwork’s public GraphQL endpoint requires an OAuth2 token. The snippet below shows how to search for new jobs, generate a bid, and submit it.
python
import time
import uuid
from datetime import datetime, timezone
UPWORK_ENDPOINT = "https://www.upwork.com/api/graphql"
UPWORK_TOKEN = os.getenv("UPWORK_ACCESS_TOKEN") # short‑lived, refresh via OAuth flow
def graphql_query(query: str, variables: Dict[str, Any] = None) -> Dict[str, Any]:
headers = {
"Authorization": f"Bearer {UPWORK_TOKEN}",
"Content-Type": "application/json",
}
payload = {"query": query, "variables": variables or {}}
r = requests.post(UPWORK_ENDPOINT, json=payload, headers=headers, timeout=10)
r.raise_for_status()
return r.json()
def find_recent_jobs(skill: str, limit: int = 5) -> list:
"""
Returns a list of job dicts posted in the last hour.
"""
query = """
query RecentJobs($skill: String!, $limit: Int!) {
jobs(query: $skill, first: $limit, sortBy: POST_DATE) {
nodes {
id
title
description
budget {
amount
currency
}
deadline
}
}
}
"""
data = graphql_query(query, {"skill": skill, "limit": limit})
return [
{
"id": n["id"],
"title": n["title"],
"description": n["description"],
"budget_min": float(n["budget"]["amount"]) if n["budget"] else None,
"budget_max": float(n["budget"]["amount"]) if n["budget"] else None,
"deadline": n["deadline"],
}
for n in data["data"]["jobs"]["nodes"]
]
def create_bid(job_id: str, cover_letter: str, amount_usd: float) -> str:
"""
Submits a proposal and returns the Upwork proposal ID.
"""
mutation = """
mutation SubmitProposal($input: SubmitProposalInput!) {
submitProposal(input: $input) {
proposalId
}
}
"""
variables = {
"input": {
"jobId": job_id,
"coverLetter": cover_letter,
"amount": amount_usd,
"currency": "USD",
}
}
resp = graphql_query(mutation, variables)
return resp["data"]["submitProposal"]["pro
Top comments (0)