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 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 take a raw prompt, run it through an LLM chain, and turn the output into a billable action on a platform like Upwork, Fiverr, or a custom gig API. The focus is on production‑grade patterns, realistic trade‑offs, and minimal hype.


1. Scope the Interaction

Before writing any code, define the contract between your agent and the platform:

Component What it does Typical API surface
Prompt → LLM Turns a natural‑language request (e.g., “write a 500‑word blog post about Rust async”) into a structured artifact (title, outline, markdown body). LangChain LLMChain, PromptTemplate.
Validator / Post‑processor Guarantees the output meets platform constraints (character limits, required fields, formatting). Simple Python functions or JSON‑Schema validators.
Gig‑platform client Authenticates, creates or updates a job proposal, uploads deliverables, and handles payment webhook. Platform‑specific REST/GraphQL SDKs (Upwork API, Fiverr Public API, or a custom webhook).
Orchestrator Retries on transient failures, respects rate limits, logs for audit. tenacity, structured logging, asyncio or Celery worker.

If any of these layers is missing, the chain will either produce unusable output or get banned for spamming.


2. Prompt Engineering – From Free Text to Structured JSON

A reliable agent needs deterministic output shapes. Instead of hoping the model returns free‑form text, we ask it to emit JSON that matches a schema.

from langchain import PromptTemplate, LLMChain
from langchain.llms import OpenAI   # swap for any provider you prefer
import json, os

# 1️⃣ Define the schema we want back
SCHEMA = {
    "title": "string",
    "outline": ["string"],          # list of section headings
    "body_md": "string",           # full markdown article
    "estimated_word_count": "int"
}

# 2️⃣ Prompt that instructs the model to output valid JSON only
template = """
You are a professional content writer.  
Given the user request below, produce a JSON object that conforms exactly to this schema:

{schema}

Do NOT add any extra text outside the JSON. If you cannot fulfill the request, return:
{{"error": "reason"}}

User request:
{request}
"""

prompt = PromptTemplate(
    input_variables=["request", "schema"],
    template=template,
)

llm = OpenAI(temperature=0.2, model_name="gpt-4-turbo", openai_api_key=os.getenv("OPENAI_API_KEY"))
chain = LLMChain(llm=llm, prompt=prompt)

def generate_article(request: str) -> dict:
    raw = chain.run(request=request, schema=json.dumps(SCHEMA, indent=2))
    try:
        data = json.loads(raw)
        if "error" in data:
            raise ValueError(data["error"])
        # Basic sanity checks
        assert isinstance(data["title"], str) and len(data["title"]) <= 120
        assert isinstance(data["body_md"], str) and len(data["body_md"].split()) >= 300
        return data
    except Exception as e:
        # In production you would send this to a dead‑letter queue for inspection
        raise RuntimeError(f"LLM output invalid: {e}") from e
Enter fullscreen mode Exit fullscreen mode

Trade‑off:

Deterministic JSON reduces parsing failures but costs a few extra tokens (the schema and instruction). With GPT‑4‑turbo the overhead is ~150 tokens per call (~$0.003), which is acceptable for most gig‑type tasks. If you need sub‑cent per call, you can switch to a smaller model (e.g., mistral-7b-instruct) and accept a higher retry rate.


3. Validation & Platform‑Specific Formatting

Each marketplace has its own rules. Upwork, for example, limits proposal titles to 80 characters and requires a cover letter in plain text. Fiverr gigs expect a package structure (basic, standard, premium).

import re

def upwork_proposal(article: dict) -> dict:
    """Transform LLM output into Upwork‑compatible fields."""
    title = article["title"][:80]                     # truncate, log if trimmed
    # Convert markdown to plain text for the cover letter (very naive)
    cover = re.sub(r"#{1,6}\s*", "", article["body_md"])  # strip heading marks
    cover = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", cover)  # strip links
    cover = cover.strip()
    if len(cover) > 1500:
        cover = cover[:1497] + "..."   # Upwork cover letter limit

    return {
        "title": title,
        "cover_letter": cover,
        "article_md": article["body_md"],   # keep for delivery after award
    }

def fiverr_gig(article: dict) -> dict:
    """Create a three‑tier Fiverr package."""
    word_count = len(article["body_md"].split())
    basic = {"words": 300, "price": 5}
    standard = {"words": 800, "price": 15}
    premium = {"words": word_count, "price": 25}
    return {
        "gig_title": article["title"][:60],
        "packages": {
            "basic": basic,
            "standard": standard,
            "premium": premium,
        },
        "description_md": article["body_md"],
    }
Enter fullscreen mode Exit fullscreen mode

Trade‑off:

Validation adds deterministic code (few microseconds) but prevents costly re‑work or account penalties. If you skip it, you risk submitting proposals that get auto‑rejected, which hurts your agent’s reputation score on the platform.


4. Talking to the Gig Platform

Most platforms expose a REST API that requires OAuth 2.0 or personal access tokens. Below is a minimal, production‑ready client for Upwork’s Freelancer endpoint (the same pattern works for Fiverr, Freelancer.com, or a custom webhook).

import httpx
from tenacity import retry, stop_after_attempt, wait_exponential

UPWORK_BASE = "https://www.upwork.com/api/v1"
TOKEN = os.getenv("UPWORK_ACCESS_TOKEN")   # scoped with `freelancer:write`

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
}

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def post_upwork_proposal(job_id: str, payload: dict) -> httpx.Response:
    url = f"{UPWORK_BASE}/jobs/{job_id}/proposals"
    resp = httpx.post(url, json=payload, headers=headers, timeout=15.0)
    # 429 = rate limit, 5xx = transient
    if resp.status_code in (429, 500, 502, 503, 504):
        resp.raise_for_status()
    resp.raise_for_status()
    return resp
Enter fullscreen mode Exit fullscreen mode

Key production considerations

Concern Mitigation
Rate limits (Upwork: 120 req/min per token) Use tenacity with exponential backoff; maintain a token bucket per API key.
Idempotency (avoid duplicate proposals) Include a UUID in a custom X-Idempotency-Key header; Upwork returns 409 if duplicate.
Error classification Separate 4xx (client) vs 5xx (server). 4xx often means malformed payload → send to DLQ for manual review.
Secrets management Never hard‑code tokens; fetch from Vault, AWS Secrets Manager, or environment variables at container start.
Observability Emit structured logs (JSON) with job_id, attempt, latency_ms, status_code. Hook into Prometheus/Grafana for alerting on error spikes.

5. Orchestrating the End‑to‑End Flow

Putting it together in an async worker (you could also run this as a Cron job or a serverless function).


python
import asyncio
import uuid
from datetime import datetime

async def handle_request(user_request: str, platform: str, external_id: str):
    """
    user_request: free‑text from a trigger (e.g., a webhook from a job board)
    platform: "upwork" or "fiverr"
    external_id: the platform‑specific job/gig identifier we are bidding on
    """
    try:
        article = generate_article(user_request)          # LLM → JSON
        if platform == "upwork":
            payload = upwork_proposal(article)
            resp = await asyncio.to_thread(post_upwork_proposal, external_id, payload)
        elif platform == "fiverr":
            # Placeholder – similar pattern with Fiverr's API
            payload = fiverr_gig(article)
            resp = await asyncio.to_thread(post_fiverr_gig, external_id, payload)
        else:
            raise ValueError(f"Unsupported platform: {payload}")

        # Log success
        print({
            "ts": datetime.utcnow().isoformat() + "Z",
            "status": resp.status_code,
            "platform": platform,
            "external_id": external_id,
Enter fullscreen mode Exit fullscreen mode

Top comments (0)