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 accept, execute, and get paid for real‑world tasks isn’t magic—it’s plumbing. Below is a step‑by‑step walkthrough of how to connect an LLM‑driven chain to a gig‑economy marketplace, the gotchas you’ll hit along the way, and code you can run today.


1. The High‑Level Flow

  1. Trigger – A client posts a job (e.g., “Write a 500‑word blog intro about renewable energy”).
  2. Ingestion – Your agent pulls the job description via the platform’s API.
  3. Planning – The LLM decides which tools (research, drafting, SEO check) are needed and in what order.
  4. Execution – Each tool runs, returns structured data, and feeds back into the LLM for the next step.
  5. Submission – The final artifact is posted back to the platform as a deliverable.
  6. Payment – Upon completion, the platform releases funds (or you invoke a micro‑payment hook like x402).

The core difficulty is stateful tool orchestration while staying within latency and cost budgets that make the agent economically viable.


2. Choosing the Stack

Concern Recommended Choice Why
LLM backbone gpt‑4‑turbo (or a self‑hosted Mixtral‑8x7B) Good trade‑off between reasoning depth and token cost for multi‑step chains.
Orchestration LangChain Expression Language (LCEL) + RunnableSequence Declarative, easy to insert custom tools, and supports async streaming.
Tool abstraction BaseTool subclasses (Python) Uniform interface for sync/async actions, automatic input validation.
Gig‑platform API REST + Webhooks (most platforms expose both) REST for pulling jobs, webhooks for push‑based status updates (reduces polling).
Payment / settlement x402 (USDC on Base) or platform escrow Enables per‑call micropayments without trusting a third‑party custodian.
Observability LangSmith + OpenTelemetry Trace token usage, latency, and tool failures in real time.

3. Skeleton Agent Code

Below is a minimal, production‑ready example that shows how to wire everything together. Replace placeholders (<…>) with your actual credentials.

# agent.py
import os
import asyncio
from typing import List, Dict

from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnableSequence, RunnableLambda
from langchain_core.tools import BaseTool
from langchain_core.messages import HumanMessage, SystemMessage
import httpx

# ----------------------------------------------------------------------
# 1️⃣ LLM
# ----------------------------------------------------------------------
llm = ChatOpenAI(
    model="gpt-4-turbo-preview",
    temperature=0.2,
    api_key=os.getenv("OPENAI_API_KEY"),
)

# ----------------------------------------------------------------------
# 2️⃣ Gig‑platform connector (example: generic REST job board)
# ----------------------------------------------------------------------
GIG_API_BASE = os.getenv("GIG_API_BASE")
GIG_API_KEY = os.getenv("GIG_API_KEY")

async def fetch_open_jobs() -> List[Dict]:
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"{GIG_API_BASE}/jobs?status=open",
            headers={"Authorization": f"Bearer {GIG_API_KEY}"},
        )
        resp.raise_for_status()
        return resp.json()["jobs"]

async def submit_deliverable(job_id: str, artifact: str) -> None:
    async with httpx.AsyncClient() as client:
        await client.post(
            f"{GIG_API_BASE}/jobs/{job_id}/deliver",
            json={"content": artifact},
            headers={"Authorization": f"Bearer {GIG_API_KEY}"},
        )

# ----------------------------------------------------------------------
# 3️⃣ Tools – research, draft, SEO check
# ----------------------------------------------------------------------
class WebSearchTool(BaseTool):
    name: str = "web_search"
    description: str = "Fetch top‑N search results for a query."

    def _run(self, query: str, *, top_k: int = 5) -> List[Dict]:
        # Placeholder: replace with your preferred search API (SerpAPI, Bing, etc.)
        raise NotImplementedError

    async def _arun(self, query: str, *, top_k: int = 5) -> List[Dict]:
        # Async wrapper – implement real call here
        return []

class DraftTool(BaseTool):
    name: str = "draft"
    description: str = "Produce a coherent paragraph given an outline."

    def _run(self, outline: str) -> str:
        prompt = [
            SystemMessage(content="You are a concise copywriter."),
            HumanMessage(content=f"Expand the following outline into a polished paragraph:\n{outline}"),
        ]
        return llm.invoke(prompt).content

    async def _arun(self, outline: str) -> str:
        return self._run(outline)

class SEOCoreTool(BaseTool):
    name: str = "seo_check"
    description: str = "Return a readability score and keyword density."

    def _run(self, text: str) -> Dict:
        # Very lightweight placeholder – replace with real library (textstat, etc.)
        return {"readability": 0.0, "keyword_density": {}}

    async def _arun(self, text: str) -> Dict:
        return self._run(text)

# ----------------------------------------------------------------------
# 4️⃣ Build the chain: plan → tool execution → refine → submit
# ----------------------------------------------------------------------
def planner(state: Dict) -> Dict:
    """Ask the LLM to decide which tool to run next."""
    messages = [
        SystemMessage(content=(
            "You are an agent that decides the next action. "
            "Output a JSON with keys: 'tool' (one of 'web_search','draft','seo_check') "
            "and 'input' (the argument for that tool)."
        )),
        HumanMessage(content=str(state)),
    ]
    decision = llm.invoke(messages).content
    # In practice, parse JSON safely; here we assume well‑formed output.
    import json
    return json.loads(decision)

def tool_executor(state: Dict) -> Dict:
    """Run the selected tool and merge its output back into state."""
    tool_name = state["tool"]
    tool_input = state["input"]
    tool_map = {
        "web_search": WebSearchTool(),
        "draft": DraftTool(),
        "seo_check": SEOCoreTool(),
    }
    tool = tool_map[tool_name]
    # Use async version if we are inside an async context; sync for simplicity here.
    result = tool._run(**tool_input) if isinstance(tool, BaseTool) else None
    return {**state, "tool_output": result}

def refine(state: Dict) -> Dict:
    """Let the LLM incorporate tool output and decide if we are done."""
    messages = [
        SystemMessage(content=(
            "You are the reasoning core. Given the current state, produce either: "
            "{'action': 'continue', 'next_tool': ..., 'next_input': ...} "
            "or {'action': 'finish', 'artifact': <final text>}."
        )),
        HumanMessage(content=str(state)),
    ]
    decision = llm.invoke(messages).content
    import json
    return json.loads(decision)

# Assemble a RunnableSequence that loops until finish.
async def run_agent(job: Dict) -> None:
    state = {"job_description": job["description"]}
    while True:
        plan = planner(state)
        state = {**state, **plan}
        state = tool_executor(state)
        decision = refine(state)
        if decision.get("action") == "finish":
            artifact = decision["artifact"]
            await submit_deliverable(job["id"], artifact)
            break
        # otherwise loop with next tool/input from decision
        state = {**state, **decision}

# ----------------------------------------------------------------------
# 5️⃣ Entry point – poll for jobs (or use webhook)
# ----------------------------------------------------------------------
async def main():
    while True:
        jobs = await fetch_open_jobs()
        for job in jobs:
            # Simple deduplication: skip if we already processed this ID.
            # In production, persist processed IDs to a DB or Redis.
            asyncio.create_task(run_agent(job))
        await asyncio.sleep(30)  # poll interval; adjust per platform rate limits

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

What the snippet does

  • Planner – asks the LLM to propose the next tool and its arguments.
  • Tool executor – runs the chosen tool (search, draft, SEO) and stores the raw output.
  • Refiner – lets the LLM decide whether the output is sufficient or if another tool is needed, ultimately emitting a final artifact.
  • Loop – repeats until the LLM signals finish.

The loop is deliberately stateless per iteration; all needed context lives in the state dict that gets threaded through the chain. This makes it easy to persist checkpoints (e.g., write state to a DB after each iteration) and resume after a crash.


4. Honest Trade‑offs

Area Benefit Cost / Gotcha
LLM‑driven planning Flexible, can adapt to unseen job types without hard‑coding flow. Each planning step adds ~150‑300 tokens; latency can exceed 2 s on slower models, making real‑time response feel sluggish.
Tool abstraction Swapping a search API or swapping in a local model is a one‑line change. Every tool call incurs an extra network hop; if you chain 4‑5 tools you may hit platform rate limits quickly.
Async polling Simple to implement; works with platforms that lack push webhooks. Polling wastes compute and may miss timely

Top comments (0)