DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms

From Prompt to Paycheck: Wiring an LLM Chain Into Real Gig Platforms

Building autonomous AI agents that can earn money on freelance marketplaces is a concrete engineering problem, not a sci‑fi fantasy. Below is a step‑by‑step walkthrough of how to connect a language‑model chain to gig‑platform APIs, add micropayment settlement via the x402 protocol, and keep the system honest about its limits.


1. High‑level Architecture

+----------------+      +----------------+      +----------------+
|  User Prompt   | -->  | LLM Orchestrator| -->| Payment Adapter |
+----------------+      +----------------+      +----------------+
        ^                         |                         |
        |                         v                         v
   (input)          +----------------+          +----------------+
   -----------------|  Gig Adapter   |----------|  x402 Settler  |
                    +----------------+          +----------------+
                                    |
                                    v
                           +----------------+
                           | Gig Platform   |
                           +----------------+
Enter fullscreen mode Exit fullscreen mode
  • LLM Orchestrator – a thin wrapper around a model (OpenAI, Anthropic, local Llama, etc.) that executes a chain of prompts, tool calls, and validation steps.
  • Gig Adapter – translates the orchestrator’s intent (e.g., “write a 500‑word blog post about Rust”) into the specific API calls a platform expects (job creation, proposal submission, file upload).
  • Payment Adapter – exposes an x402‑compatible endpoint so the platform (or a escrow service) can charge the agent per successful completion.

2. Prompt Design & LLM Chain

We’ll use LangChain‑style composable prompts because they let us isolate concerns: task understanding, output generation, and self‑check.

# llm_chain.py
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain, SequentialChain

llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0.2)

# 1️⃣ Understand the gig description
understand_prompt = PromptTemplate(
    input_variables=["gig_desc"],
    template="""
    You are a senior freelancer. Parse the following gig description and output a JSON object:
    - title: concise job title
    - deliverables: list of concrete items to produce
    - constraints: any length, tone, format, or deadline notes
    Gig description: {gig_desc}
    """
)
understand_chain = LLMChain(llm=llm, prompt=understand_prompt, output_key="spec")

# 2️⃣ Produce the deliverable (example: blog post)
write_prompt = PromptTemplate(
    input_variables=["spec"],
    template="""
    Using the spec below, write a {deliverables[0]} that satisfies all constraints.
    Spec: {spec}
    """
)
write_chain = LLMChain(llm=llm, prompt=write_prompt, output_key="draft")

# 3️⃣ Self‑check (simple length & profanity filter)
check_prompt = PromptTemplate(
    input_variables=["draft", "spec"],
    template="""
    Review the draft against the spec. Return "OK" if it meets length, tone, and format constraints;
    otherwise return a short list of what needs fixing.
    Draft: {draft}
    Spec: {spec}
    """
)
check_chain = LLMChain(llm=llm, prompt=check_prompt, output_key="review")

# Assemble the chain
overall_chain = SequentialChain(
    chains=[understand_chain, write_chain, check_chain],
    input_variables=["gig_desc"],
    output_variables=["spec", "draft", "review"],
    verbose=True,
)
Enter fullscreen mode Exit fullscreen mode

Trade‑offs

Aspect Choice Why Cost / Risk
Model size gpt-4o-mini (or comparable 7B‑13B open source) Good trade‑off between quality and latency for short‑form content. Slightly higher per‑token cost vs. smaller models; may miss niche style nuances.
Temperature 0.2 Keeps output deterministic enough for automated QA while allowing creativity. Lower temperature can make the model overly rigid; higher temperature risks off‑spec output.
Chain length 3 steps (understand → write → review) Isolation makes debugging easier and lets us swap components. Adds ~2‑3 model calls per job → higher latency and cost.

If you need stricter guarantees (e.g., code that must compile), replace the review step with a deterministic validator (linter, unit test runner) instead of another LLM call.


3. Gig Platform Adapter (Upwork Example)

Most freelance sites expose a REST API that requires OAuth2. The adapter below shows the minimal flow: fetch open jobs, submit a proposal with the LLM‑generated draft, and mark the job as “completed” once the client approves.

# upwork_adapter.py
import os
import requests
from typing import Dict, 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")
ACCESS_TOKEN = os.getenv("UPWORK_ACCESS_TOKEN")  # obtained via OAuth flow once

BASE_URL = "https://www.upwork.com/api/v1"

def _auth_header() -> Dict[str, str]:
    return {"Authorization": f"Bearer {ACCESS_TOKEN}"}

def fetch_open_jobs(keywords: str = "blog post", page: int = 0) -> List[Dict]:
    params = {
        "q": keywords,
        "page": page,
        "sort": "recency",
        "contract_type": "hourly",  # or "fixed"
    }
    resp = requests.get(f"{BASE_URL}/jobs/search", headers=_auth_header(), params=params)
    resp.raise_for_status()
    return resp.json().get("jobs", [])

def submit_proposal(job_id: str, cover_letter: str, bid_amount: float) -> Dict:
    payload = {
        "job_id": job_id,
        "cover_letter": cover_letter,
        "bid_amount": bid_amount,
        "duration": "short_term",
    }
    resp = requests.post(
        f"{BASE_URL}/proposals",
        headers={**_auth_header(), "Content-Type": "application/json"},
        json=payload,
    )
    resp.raise_for_status()
    return resp.json()

def mark_job_completed(job_id: str, work_delivered: str) -> Dict:
    payload = {"work_delivered": work_delivered, "status": "completed"}
    resp = requests.post(
        f"{BASE_URL}/jobs/{job_id}/close",
        headers=_auth_header(),
        json=payload,
    )
    resp.raise_for_status()
    return resp.json()
Enter fullscreen mode Exit fullscreen mode

Honest notes

  • Upwork’s API is rate‑limited (≈120 requests/min per token). A burst of proposals will trigger HTTP 429; implement exponential back‑off or a token bucket.
  • The platform requires a verified payment method on the freelancer side before you can withdraw earnings; the adapter does not handle that.
  • Job descriptions vary wildly; the “understand” LLM step helps, but you’ll still need a fallback to skip jobs with ambiguous specs (e.g., “help me with my website”).

4. x402 Payment Adapter

The x402 spec lets you attach a lightweight HTTP header (X-Payment-Request: …) to a response, letting the caller settle with a cryptographic proof. For our use case we expose a /settle endpoint that the gig platform (or an escrow service) calls after the agent signals completion.


python
# x402_settler.py
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
import os
import base64
import hashlib
import time

app = FastAPI()

# Shared secret known only to the agent and the escrow service.
# In production, derive this from a Diffie‑Hellman exchange or a vault.
X402_SECRET = os.getenv("X402_SECRET", "change-me-to-a-32-byte-random")

class SettlementRequest(BaseModel):
    job_id: str
    amount_usdc: float   # e.g., 0.05
    nonce: str           # provided by caller to prevent replay

def _make_payment_request(amount: float, nonce: str) -> str:
    """
    Returns a base64‑encoded string: HMAC_SHA256(secret, job_id|amount|nonce|timestamp)
    """
    timestamp = str(int(time.time()))
    msg = f"{job_id}|{amount}|{nonce}|{timestamp}".encode()
    mac = hashlib.pbkdf2_hmac(
        "sha256", X402_SECRET.encode(), msg, 1000
    )
    return base64.urlsafe_b64encode(mac).decode()

@app.post("/settle")
async def settle(
    req: SettlementRequest,
    x_payment_request: str = Header(None),
):
    # Re‑compute the MAC and verify
    expected = _make_payment_request(req.amount_usdc, req.nonce)
    if not x_payment_request or not secrets.compare_digest(x_payment_request, expected):
Enter fullscreen mode Exit fullscreen mode

Top comments (0)