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 agents that can discover, bid on, and complete micro‑tasks on gig marketplaces is a concrete engineering problem. Below is a walk‑through of the pieces that actually make it work, the trade‑offs you’ll encounter, and ready‑to‑run Python snippets you can adapt.


1. Architecture Overview

+----------------+      +----------------+      +-----------------+
|   Gig Platform | <--->|   Agent Core   | <--->|   LLM Service   |
|   (REST/GraphQL)|      | (orchestrator) |      | (local or API) |
+----------------+      +----------------+      +-----------------+
        ^                         ^                         ^
        |                         |                         |
   Webhooks/ Polling        Decision Engine          Prompt + Tools
Enter fullscreen mode Exit fullscreen mode
  • Gig Platform – the marketplace you target (e.g., a custom API that mirrors Upwork’s “submit proposal” endpoint).
  • Agent Core – a thin service that watches for new tasks, builds a prompt, calls the LLM, validates the answer, and issues the platform API call.
  • LLM Service – can be a hosted API (OpenAI, Anthropic) or a self‑hosted model served via TGI/vLLM. The choice impacts latency, cost, and data‑privacy constraints.

The core loop is:

  1. Fetch a new task (title, description, budget, required skills).
  2. Prompt the LLM to produce a structured proposal (cover letter, price, timeline).
  3. Parse the LLM output into a JSON payload that matches the platform’s schema.
  4. POST the payload to the platform’s “create proposal” endpoint.
  5. Handle the response (success, retry, or fallback).

2. Choosing the LLM

Option Latency (typical) Cost per 1k tokens Data privacy Customization
Hosted API (e.g., GPT‑4‑turbo) 300‑800 ms $0.01‑$0.03 Data leaves your VPC Limited to prompt engineering
Self‑hosted 7B/13B (Llama‑2, Mistral) 150‑400 ms (GPU) $0 (compute) Data stays on‑prem Full fine‑tuning possible
Hybrid (API for reasoning, local for extraction) Varies Mixed Mixed Best of both worlds

For a gig‑agent that must run 24/7 and keep cost predictable, a self‑hosted 7B model on a modest GPU (e.g., an A10G) is often the sweet spot. You still need to budget for GPU hourly rates, but you avoid per‑call token fees and can keep sensitive job data inside your VPC.


3. Building the Prompt Chain

We’ll use LangChain‑style primitives without pulling in the whole library—just enough to illustrate the flow. The goal is to turn a free‑form gig description into a deterministic JSON object.

import json
import textwrap
from typing import Dict, Any

# ------------------------------------------------------------------
# 1️⃣ Prompt template – instruct the model to output valid JSON only
# ------------------------------------------------------------------
PROMPT_TEMPLATE = textwrap.dedent("""
    You are a freelance proposal writer. Given the gig details below,
    produce a JSON object with the exact keys: 
    "cover_letter", "price_usd", "estimated_days", "relevant_skills".
    Do NOT add any extra text outside the JSON.

    Gig:
    Title: {title}
    Description: {description}
    Budget: {budget}
    Required Skills: {skills}
""").strip()

def build_prompt(gig: Dict[str, Any]) -> str:
    return PROMPT_TEMPLATE.format(
        title=gig["title"],
        description=gig["description"],
        budget=gig["budget"],
        skills=", ".join(gig["skills"])
    )
Enter fullscreen mode Exit fullscreen mode

Why enforce JSON?

  • Guarantees the downstream parser won’t fail on stray prose.
  • Lets us use function‑calling or grammar‑constrained decoding (e.g., llama.cpp with a JSON grammar) to further reduce hallucination.
  • Makes unit testing trivial: you can compare the parsed dict against an expected schema.

2️⃣ Calling the model

Below is a minimal wrapper for a self‑hosted TGI endpoint. Swap the URL and auth for an OpenAI‑compatible API if you prefer.

import requests
from requests.exceptions import RequestException

TGI_URL = "http://llm-service.internal:8080/generate"

def query_llm(prompt: str, max_new_tokens: int = 256) -> str:
    payload = {
        "inputs": prompt,
        "parameters": {
            "max_new_tokens": max_new_tokens,
            "temperature": 0.2,      # low temp for deterministic output
            "stop": ["\n\n"]         # stop at blank line to avoid extra chatter
        }
    }
    try:
        resp = requests.post(TGI_URL, json=payload, timeout=10)
        resp.raise_for_status()
        return resp.json()["generated_text"]
    except RequestException as exc:
        # In production you’d push this to a dead‑letter queue or alerting system
        raise RuntimeError(f"LLM call failed: {exc}") from exc
Enter fullscreen mode Exit fullscreen mode

3️⃣ Parsing & validation

def parse_proposal(raw: str) -> Dict[str, Any]:
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValueError(f"LLM did not return valid JSON: {raw[:200]}") from exc

    required = {"cover_letter", "price_usd", "estimated_days", "relevant_skills"}
    missing = required - data.keys()
    if missing:
        raise ValueError(f"Missing fields: {missing}")

    # Light‑weight type coercion & sanity checks
    data["price_usd"] = float(data["price_usd"])
    data["estimated_days"] = int(data["estimated_days"])
    data["relevant_skills"] = list(map(str, data["relevant_skills"]))
    return data
Enter fullscreen mode Exit fullscreen mode

4. Integrating With a Gig Platform API

Assume the platform exposes a REST endpoint POST /api/v1/proposals that expects:

{
  "gig_id": "string",
  "cover_letter": "string",
  "price_usd": "number",
  "estimated_days": "integer",
  "skills": ["string", ...],
  "worker_id": "string"   // your agent's identifier on the platform
}
Enter fullscreen mode Exit fullscreen mode
PLATFORM_BASE = "https://gig.example.com/api/v1"
WORKER_ID = "agent-42"
PLATFORM_TOKEN = "Bearer <your‑platform‑jwt>"

def submit_proposal(gig_id: str, proposal: Dict[str, Any]) -> Dict[str, Any]:
    url = f"{PLATFORM_BASE}/proposals"
    payload = {
        "gig_id": gig_id,
        "worker_id": WORKER_ID,
        "cover_letter": proposal["cover_letter"],
        "price_usd": proposal["price_usd"],
        "estimated_days": proposal["estimated_days"],
        "skills": proposal["relevant_skills"]
    }
    headers = {"Authorization": PLATFORM_TOKEN, "Content-Type": "application/json"}
    try:
        r = requests.post(url, json=payload, headers=headers, timeout=8)
        r.raise_for_status()
        return r.json()   # typically contains proposal_id, status, etc.
    except RequestException as exc:
        # Surface the HTTP status for retry logic
        raise RuntimeError(f"Platform API error {r.status_code if 'r' in locals() else 'N/A'}: {exc}") from exc
Enter fullscreen mode Exit fullscreen mode

Retry & back‑off strategy

  • Transient network errors – exponential backoff (2 s, 4 s, 8 s) up to three attempts.
  • 429 Too Many Requests – respect the Retry-After header or default to 60 s.
  • 5xx from platform – treat as transient; same back‑off.
  • 4xx (bad request) – likely a schema mismatch; log the payload and send to a dead‑letter queue for manual inspection.

5. Honest Trade‑offs

Dimension What you gain What you lose / need to manage
Model size vs. latency Larger models (13B+) produce richer proposals but add 20

Top comments (0)