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

For developers who want to turn a language‑model prompt into a billable service on existing freelance marketplaces.


Why bother chaining LLMs to gig platforms?

Gig sites already expose REST or GraphQL endpoints for job posting, proposal submission, and payment handling. If you can make an autonomous agent that:

  1. Reads a new job description (via the platform’s feed API)
  2. Generates a tailored proposal (using an LLM chain)
  3. Submits the proposal and, if accepted, delivers the work (again via API calls)

…then you’ve turned a prompt into a repeatable, paid workflow. The upside is clear: you can scale micro‑tasks without manual oversight. The downside is equally real: you inherit the platform’s latency, rate limits, and policy constraints, and you must absorb the cost of LLM inference yourself.

Below is a concrete, minimal‑working example that shows how to stitch those pieces together using Python, the LangChain expression language, and a hypothetical gig‑platform SDK. Adjust the imports and credentials to match the platform you target (Upwork, Fiverr, Fiverr Business, etc.).


1. Scaffolding the environment

# Create a fresh venv
python -m venv .venv
source .venv/bin/activate

# Install core deps
pip install langchain openai requests python-dotenv
Enter fullscreen mode Exit fullscreen mode

Create a .env file (never commit this) with:

OPENAI_API_KEY=sk-...
GIG_PLATFORM_TOKEN=your_platform_personal_access_token
GIG_PLATFORM_BASE_URL=https://api.examplegig.com/v1
Enter fullscreen mode Exit fullscreen mode

Load them in code:

import os
from dotenv import load_dotenv
load_dotenv()

OPENAI_KEY = os.getenv("OPENAI_API_KEY")
PLATFORM_TOKEN = os.getenv("GIG_PLATFORM_TOKEN")
PLATFORM_BASE = os.getenv("GIG_PLATFORM_BASE_URL")
Enter fullscreen mode Exit fullscreen mode

2. Pulling new jobs from the platform

Most platforms expose a “feed” endpoint that returns JSON with fields like id, title, description, budget, skills. The following helper abstracts pagination and basic error handling:

import requests
from typing import List, Dict

def fetch_new_jobs(since_id: int | None = None) -> List[Dict]:
    """Return a list of job dicts newer than `since_id`."""
    headers = {"Authorization": f"Bearer {PLATFORM_TOKEN}"}
    params = {"per_page": 100}
    if since_id:
        params["since_id"] = since_id

    url = f"{PLATFORM_BASE}/jobs"
    resp = requests.get(url, headers=headers, params=params, timeout=10)
    resp.raise_for_status()
    data = resp.json()
    # Assume API returns {"jobs": [...], "next_page_token": "..."}
    return data.get("jobs", [])
Enter fullscreen mode Exit fullscreen mode

Trade‑off: Polling every 30‑60 s is simple but wastes bandwidth. If the platform offers webhooks, switch to those to cut latency and cost—just remember to verify signatures.


3. Building the proposal‑generation chain

We’ll use LangChain’s LLMChain with a prompt that asks the model to:

  • Restate the client’s needs in plain language.
  • Highlight relevant skills from the agent’s profile.
  • Propose a concise deliverable timeline and price (within the client’s budget).
from langchain import OpenAI, PromptTemplate, LLMChain

llm = OpenAI(temperature=0.3, openai_api_key=OPENAI_KEY)  # cost‑effective davinci‑002

PROPOSAL_TEMPLATE = """
You are an autonomous freelancer agent. Given the job description below, write a short proposal
(150‑200 words) that:
1. Summarizes the client's goal.
2. Lists two specific skills you have that match the request.
3. Suggests a realistic timeline (in days) and a price that does not exceed the client's budget.
4. Ends with a call‑to‑action asking the client to confirm details.

Job:
{job_desc}

Proposal:
"""

prompt = PromptTemplate(
    input_variables=["job_desc"],
    template=PROPOSAL_TEMPLATE.strip(),
)

proposal_chain = LLMChain(llm=llm, prompt=prompt)
Enter fullscreen mode Exit fullscreen mode

Honest note: Temperature 0.3 keeps output deterministic enough for repeatable pricing, but you’ll still see occasional hallucinations (e.g., inventing a skill). Add a simple validation step after generation (see §4).


4. Validation & safety net

Before sending anything to the platform, run a lightweight sanity check:

import re

def validate_proposal(text: str, max_budget: float) -> str | None:
    """Return None if proposal looks good, otherwise an error message."""
    # 1. Length check
    if not (150 <= len(text.split()) <= 250):
        return "Word count out of range."

    # 2. Price extraction – look for a number preceded by $ or USD
    price_match = re.search(r'\$?\s?(\d+(?:\.\d+)?)\s*(?:USD)?', text, re.I)
    if price_match:
        proposed_price = float(price_match.group(1))
        if proposed_price > max_budget + 5:  # allow $5 tolerance for rounding
            return f"Proposed price ${proposed_price:.2f} exceeds budget ${max_budget:.2f}."
    else:
        return "No detectable price in proposal."

    # 3. Profanity / policy filter (very basic)
    banned = ["spam", "scam", "guaranteed"]
    if any(b in text.lower() for b in banned):
        return "Potential policy violation detected."

    return None
Enter fullscreen mode Exit fullscreen mode

If validate_proposal returns a message, you can either retry the chain with a stricter prompt or discard the job and log the failure for manual review.


5. Submitting the proposal

Assuming the platform’s API expects a JSON payload { "job_id": "...", "cover_letter": "..." }:

def submit_proposal(job_id: int, cover_letter: str) -> Dict:
    url = f"{PLATFORM_BASE}/proposals"
    payload = {"job_id": job_id, "cover_letter": cover_letter}
    headers = {"Authorization": f"Bearer {PLATFORM_TOKEN}", "Content-Type": "application/json"}
    resp = requests.post(url, json=payload, headers=headers, timeout=10)
    resp.raise_for_status()
    return resp.json()
Enter fullscreen mode Exit fullscreen mode

Wrap the whole flow in a worker loop:

import time

def run_agent(poll_interval: int = 45):
    last_seen = None
    while True:
        try:
            jobs = fetch_new_jobs(since_id=last_seen)
            for job in jobs:
                # Skip if we already processed this ID (simple dedup)
                if last_seen and job["id"] <= last_seen:
                    continue

                # Generate proposal
                raw = proposal_chain.run(job_desc=job["description"])
                err = validate_proposal(raw, max_budget=job.get("budget", 0))
                if err:
                    print(f"[{job['id']}] Validation failed: {err}")
                    continue

                # Submit
                result = submit_proposal(job["id"], raw)
                print(f"[{job['id']}] Proposal submitted: {result.get('proposal_id')}")
                last_seen = max(last_seen or 0, job["id"])

        except Exception as e:
            print(f"Error in agent loop: {e}")

        time.sleep(poll_interval)

if __name__ == "__main__":
    run_agent()
Enter fullscreen mode Exit fullscreen mode

Trade‑offs highlighted

Aspect Choice Cost / Risk Mitigation
LLM model text-davinci-002 via OpenAI API ~$0.02 per 1k tokens (≈$0.004 per proposal) Cache similar prompts; switch to a smaller open‑source model if volume grows.
Polling vs webhooks Simple polling (45 s) Extra requests, possible rate‑limit hits Implement exponential backoff; migrate to webhooks when platform supports them.
Validation Regex price scan + word count May miss sophisticated policy violations Add a lightweight moderation API (e.g., OpenAI Moderation) as a second line.
Error handling Generic try/except with retry loop Could hide permanent failures (bad credentials) Separate transient (network) vs permanent (auth) errors; alert on the latter.
Payment Not shown; assumes platform pays after work is delivered You must still invoice or rely on platform escrow Integrate the platform’s payment webhook to auto‑confirm completion before requesting payout.

6. Delivering the work (optional stub)

If the job is “write a 300‑word blog post”, you can reuse the same LLM chain with a different prompt, store the output, and then call the platform’s “submit work” endpoint:

def generate_article(topic: str, length: int = 300) -> str:
    article_prompt = f"Write a {length}-word, SEO‑friendly blog post about {topic}."
    return llm.invoke(article_prompt)

def submit_work(job_id: int, artifact: str) -> Dict:
    url = f"{PLATFORM_BASE}/jobs/{job_id}/deliverables"
    payload = {"content": artifact}
    # ... same auth/header logic as submit_proposal
Enter fullscreen mode Exit fullscreen mode

Again, validate length, copyscape‑like similarity, and any client‑provided style guide before hitting submit.


7. Observability & ops

  • Logging – JSON logs with fields: timestamp, job_id, action, latency_ms, token_usage, `out

Top comments (0)