From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms
Building autonomous agents that can actually earn money on freelance marketplaces requires more than a clever prompt. Below is a pragmatic walk‑through of the components, the code that glues them together, and the practical trade‑offs you’ll encounter when you try to move from “it works in a notebook” to a service that gets paid in USDC on Base.
1. High‑level Architecture
+-------------------+ +-------------------+ +-------------------+
| User / Trigger | ---> | Prompt Builder | ---> | LLM Chain |
+-------------------+ +-------------------+ +-------------------+
| |
v v
+----------------+ +-------------------+
| Output Parser | | Gig‑API Adapter |
+----------------+ +-------------------+
| |
v v
+----------------+ +-------------------+
| Payment Hook | <----> | x402 Settlement |
+----------------+ +-------------------+
- Prompt Builder – turns a high‑level intent (e.g., “write a 500‑word blog intro about renewable energy”) into a structured prompt that the LLM can consume.
- LLM Chain – a sequence of calls (sometimes with retrieval, sometimes with tool use) that produces the final artifact.
- Output Parser – validates shape, length, and format; throws an error if the LLM hallucinates something unusable.
- Gig‑API Adapter – authenticates to the marketplace, creates a job/gig submission, and polls for status.
- Payment Hook – invokes the x402 settlement flow once the buyer confirms delivery; the agent receives USDC directly to its wallet.
Each block is deliberately isolated so you can swap‑out components (e.g., replace the LLM with a local model, or change the gig platform) without rewriting the whole pipeline.
2. Prompt Engineering & LLM Chain
We’ll use LangChain for its composable primitives, but the same ideas apply to LlamaIndex or a hand‑rolled prompt‑template library.
# prompt_builder.py
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI # swap for any compatible endpoint
# 1️⃣ Base template – keep it short, explicit, and versioned.
BASE_TEMPLATE = """
You are a professional freelance writer.
Task: {task_description}
Constraints:
- Length: {min_words}-{max_words} words
- Tone: {tone}
- Format: {output_format}
Do not add any preamble or apology. Return only the requested content.
"""
prompt = PromptTemplate(
input_variables=["task_description", "min_words", "max_words", "tone", "output_format"],
template=BASE_TEMPLATE,
)
# 2️⃣ LLM – choose a model whose cost per token fits your pricing window.
llm = ChatOpenAI(
model_name="gpt-4o-mini", # cheap enough for $0.01‑$0.10 per call
temperature=0.3,
max_tokens=800, # safety net to avoid runaway generation
)
# 3️⃣ Chain – you can insert retrieval, tool use, or self‑critique steps here.
writer_chain = LLMChain(llm=llm, prompt=prompt)
Why this works (and where it frays)
| Aspect | Benefit | Trade‑off |
|---|---|---|
| Explicit constraints in the prompt reduce hallucination and make output parsing easier. | Still relies on the model to obey length/tone; occasional overruns happen. | |
| Low‑cost model (gpt‑4o‑mini) keeps per‑call cost in the target band. | Slightly lower reasoning depth; complex multi‑step tasks may need a stronger model, raising cost. | |
| Deterministic temperature (0.3) gives repeatable outputs for testing. | Too low temperature can make the output feel stale; you may need to tune per‑task. |
If you need retrieval (e.g., pulling a style guide from a knowledge base), slot a RetrievalQA step before the LLMChain. If you want the model to call external tools (e.g., a grammar checker), wrap the chain in an Agent that can invoke those tools between LLM calls.
3. Parsing & Validation
The gig platform will reject malformed submissions, so we validate before we ever hit their API.
# output_parser.py
import re
from pydantic import BaseModel, ValidationError, field_validator
class GigOutput(BaseModel):
text: str
word_count: int
@field_validator("text")
@classmethod
def not_empty(cls, v):
if not v.strip():
raise ValueError("Output text is empty")
return v
@field_validator("word_count")
@classmethod
def matches_text(cls, v, info):
actual = len(info.data["text"].split())
if actual != v:
raise ValueError(f"Declared word count {v} does not match actual {actual}")
return v
def parse_and_validate(raw: str, min_words: int, max_words: int) -> GigOutput:
# Very naive word count; replace with a proper tokenizer if needed.
wc = len(raw.split())
if not (min_words <= wc <= max_words):
raise ValueError(f"Word count {wc} outside allowed range [{min_words},{max_words}]")
return GigOutput(text=raw.strip(), word_count=wc)
Observations
- The parser catches the two most common failure modes: empty output and length mismatch.
- Adding a schema (Pydantic) gives you automatic JSON‑serialization for later storage or audit logs.
- If you need richer structure (e.g., markdown with front‑matter), extend the model accordingly—just keep the validation logic tight; otherwise you’ll spend more time debugging parsing errors than actually earning.
4. Gig‑Platform Adapter (Fiverr Example)
Most marketplaces expose a REST API for creating a “gig” or submitting a proposal. Below is a minimal adapter for Fiverr’s Seller API (you’ll need an API key and OAuth token). Replace the endpoint and payload fields for Upwork, Freelancer, or a custom gig board.
# fiverr_adapter.py
import httpx
from typing import Dict
FIVERR_BASE = "https://api.fiverr.com/v1"
class FiverrAdapter:
def __init__(self, access_token: str):
self.client = httpx.AsyncClient(
base_url=FIVERR_BASE,
headers={"Authorization": f"Bearer {access_token}"},
timeout=httpx.Timeout(10.0, read=30.0),
)
async def create_gig(
self,
title: str,
description: str,
category_id: int,
price_usd: float,
) -> Dict:
payload = {
"title": title,
"description": description,
"category_id": category_id,
"price": {
"amount": round(price_usd, 2),
"currency": "USD",
},
# Optional: delivery time, revisions, etc.
}
resp = await self.client.post("/gigs", json=payload)
resp.raise_for_status()
return resp.json()
async def get_gig_status(self, gig_id: str) -> Dict:
resp = await self.client.get(f"/gigs/{gig_id}")
resp.raise_for_status()
return resp.json()
Key points
- Async client – lets you pipeline multiple LLM calls while waiting for the platform’s rate‑limited endpoints.
- Explicit timeout – prevents a hung request from blocking your event loop.
-
Error propagation –
raise_for_status()turns HTTP 4xx/5xx into exceptions that your orchestrator can catch and turn into a payment‑refund or retry logic.
5. x402 Payment Settlement
The x402 protocol lets you attach a micropayment to an HTTP response. When the buyer confirms delivery (via a webhook you expose), you respond with an x402 header that triggers the settlement on Base. The snippet below shows how to construct that response using the x402-py helper library (a thin wrapper around the spec).
python
# payment_hook.py
from x402 import create_payment_response, PaymentRequired
from eth_account import Account
import os
# Your agent’s wallet – keep the private key in a secret manager, never in code.
WALLET_PRIVATE_KEY = os.getenv("AGENT_WALLET_PRIV")
ACCOUNT = Account.from_key(WALLET_PRIVATE_KEY) if WALLET_PRIVATE_KEY else None
async def settle_payment(amount_usdc: float, buyer_address: str) -> dict:
"""
Returns a dict suitable for sending as an HTTP response body.
The caller must attach the `x402` header returned by create_payment_response.
"""
if ACCOUNT is None:
raise RuntimeError("Wallet not configured")
# x402 expects amount in the smallest unit (USDC has 6 decimals)
amount_wei = int(amount_usdc * 1_000_000)
# Build the payment request – the buyer will sign and send the transaction.
payment_req = PaymentRequired(
amount=amount_wei,
asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base
payee=ACCOUNT.address,
chain
Top comments (0)