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

Target audience: developers building autonomous AI agents that need to earn money by completing gigs on platforms such as Upwork, Fiverr, or custom marketplaces.


1. Why Wire an LLM Chain to a Gig Platform?

Gig platforms expose APIs (or webhooks) that let you create proposals, submit work, and receive payments programmatically. An LLM chain can turn a natural‑language gig description into a structured proposal, a price quote, or even a slice of code. The value proposition is simple: automate the repetitive, language‑heavy steps of bidding and delivering while keeping a human in the loop for oversight.

The trade‑off is that you inherit the platform’s rate limits, authentication complexity, and the nondeterministic nature of LLMs. You’ll need to budget for API calls, handle failures gracefully, and monitor cost‑per‑task.


2. High‑Level Architecture

+-------------------+        +---------------------+        +-------------------+
|  Gig Platform API | <--->  |  Agent Service (FastAPI) | <--->  |  LLM Chain (LangChain) |
+-------------------+        +---------------------+        +-------------------+
        ^                         ^                               ^
        |                         |                               |
   Webhook/Callback          Auth & Rate‑limit          Prompt, Tools, Memory
Enter fullscreen mode Exit fullscreen mode
  • Agent Service – a thin HTTP wrapper that authenticates to the gig platform, validates incoming webhook payloads, and forwards the relevant fields to the LLM chain.
  • LLM Chain – a LangChain‑style pipeline that (1) extracts structured data from the gig description, (2) runs any needed tools (e.g., a code sandbox, a price calculator), and (3) renders a final answer (proposal text, JSON payload, etc.).
  • Observability – structured logging, Prometheus metrics for latency/token usage, and dead‑letter queues for failed platform calls.

3. Prompt Engineering & Tool Integration

Below is a minimal, production‑ready chain that turns a gig description into a Upwork‑compatible proposal. We keep the prompt explicit, avoid chain‑of‑thought tricks that add latency, and use a single LLM call to control cost.

# llm_chain.py
from langchain import PromptTemplate, LLMChain
from langchain.chat_models import ChatOpenAI   # swap for any OpenAI‑compatible endpoint
from pydantic import BaseModel, Field
import json

class ProposalInput(BaseModel):
    title: str = Field(..., description="Gig title")
    description: str = Field(..., description="Full gig description")
    budget: str | None = Field(None, description="Client‑stated budget, if any")

class ProposalOutput(BaseModel):
    cover_letter: str
    suggested_price: float | None
    estimated_hours: float | None

PROMPT_TEMPLATE = """
You are a freelance assistant. Given the gig details below, produce a concise cover letter
(max 150 words), a suggested price in USD, and an estimated number of hours to complete.
If the client already gave a budget, respect it unless you believe the scope is mismatched.
Return ONLY a JSON object with keys: cover_letter, suggested_price, estimated_hours.

Gig Title: {title}
Gig Description: {description}
Client Budget: {budget}
"""

prompt = PromptTemplate(
    input_variables=["title", "description", "budget"],
    template=PROMPT_TEMPLATE,
)

llm = ChatOpenAI(model_name="gpt-4-turbo", temperature=0.2, max_tokens=250)

proposal_chain = LLMChain(llm=llm, prompt=prompt, output_key="raw_json")

def run_proposal(title: str, description: str, budget: str | None = None) -> ProposalOutput:
    raw = proposal_chain.run(
        title=title,
        description=description,
        budget=budget or "Not specified"
    )
    # The LLM is instructed to output pure JSON; we guard against stray text.
    try:
        data = json.loads(raw.strip())
    except json.JSONDecodeError:
        # Fallback: ask the model to re‑try with a stricter prompt (costly, rare)
        raise ValueError("LLM did not return valid JSON")
    return ProposalOutput(**data)
Enter fullscreen mode Exit fullscreen mode

Trade‑offs visible here

Aspect Choice Reason Cost / Risk
Model gpt-4-turbo (≈ $0.03/1k tokens) Highest quality for proposal writing; reduces rework. Higher per‑call cost vs. a smaller model.
Prompt length ~120 tokens Keeps latency low (< 500 ms typical). Less room for nuance; may need manual tweak for exotic gigs.
Output format Strict JSON Enables direct Pydantic validation, eliminates post‑processing. If the model drifts, you get a JSON decode error → retry logic needed.
Temperature 0.2 Produces deterministic, business‑like language. Slightly less creative; may miss niche phrasing that wins bids.

If you need to cut cost, swap gpt-4-turbo for gpt-3.5-turbo (~$0.002/1k tokens) and accept a modest drop in proposal quality. You can also cache recent proposals for identical titles to avoid duplicate LLM calls.


4. Hooking Into a Gig Platform (Upwork Example)

Upwork’s public API requires OAuth 2.0. The snippet below shows a FastAPI endpoint that receives a webhook from Upwork when a new job is posted, runs the LLM chain, and submits a proposal.

# agent_service.py
import os
import httpx
from fastapi import FastAPI, Header, HTTPException, Request
from pydantic import BaseModel
from llm_chain import run_proposal, ProposalInput, ProposalOutput

app = FastAPI()
UPWORK_CLIENT_ID = os.getenv("UW_CLIENT_ID")
UPWORK_CLIENT_SECRET = os.getenv("UW_CLIENT_SECRET")
UPWORK_REDIRECT_URI = os.getenv("UW_REDIRECT_URI")
# In practice you would store/retrieve a fresh access token via refresh flow.
ACCESS_TOKEN = os.getenv("UW_ACCESS_TOKEN")  # placeholder

class UpworkJob(BaseModel):
    job_id: str
    title: str
    description: str
    budget: str | None = None
    # other fields omitted for brevity

@app.post("/upwork/webhook")
async def upwork_webhook(
    request: Request,
    x_upwork_signature: str = Header(None),
):
    # 1️⃣ Verify signature (HMAC‑SHA256 using client secret) – omitted for brevity.
    payload = await request.json()
    job = UpworkJob(**payload)

    # 2️⃣ Run LLM chain
    try:
        proposal: ProposalOutput = run_proposal(
            title=job.title,
            description=job.description,
            budget=job.budget,
        )
    except Exception as exc:
        # Log and return 500 so Upwork can retry later.
        raise HTTPException(status_code=500, detail=str(exc)) from exc

    # 3️⃣ Build Upwork proposal payload (per their API spec)
    proposal_payload = {
        "cover_letter": proposal.cover_letter,
        "amount": str(proposal.suggested_price or 0),
        "contract_length": "One‑time project",
        # Upwork expects a category/subcategory – you’d map from job tags.
        "category2": "IT & Networking",
        "subcategory2": "Web Development",
    }

    # 4️⃣ Call Upwork API
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"https://www.upwork.com/api/profiles/v2/jobs/{job.job_id}/proposals",
            json=proposal_payload,
            headers={
                "Authorization": f"Bearer {ACCESS_TOKEN}",
                "Content-Type": "application/json",
            },
            timeout=10.0,
        )
        if resp.status_code >= 300:
            # Propagate error so monitoring can alert.
            raise HTTPException(status_code=502, detail=resp.text)

    return {"status": "proposal_sent", "upwork_response": resp.json()}
Enter fullscreen mode Exit fullscreen mode

Observations & Trade‑offs

  • Authentication – Upwork’s OAuth flow is stateful; you must store refresh tokens securely and handle 401 responses by re‑authenticating. In a serverless environment you might keep tokens in an encrypted DynamoDB item or Vault.
  • Rate limits – Upwork allows ~10 proposals/min per token. If you anticipate bursty traffic, queue incoming webhooks (e.g., via AWS SQS) and process with a worker pool that respects the limit via a token‑bucket algorithm.
  • Error handling – Distinguish transient errors (network, 5xx) from permanent ones (validation failures). Transient errors can be retried with exponential backoff; permanent errors should be logged and possibly forwarded to a human‑in‑the‑loop queue.
  • Latency – The LLM call dominates (~300‑600 ms). Adding a synchronous HTTP round‑trip to Upwork adds another 100‑200 ms. End

Top comments (0)