Skip to content

Python SDK ​

The uvd-em-sdk Python package provides a clean async client for the Execution Market REST API.

Async, typed (Pydantic v2), and namespaced by resource (Stripe pattern): client.tasks, client.submissions, client.workers, and so on. See Plugin SDK for the full namespace table.

Installation ​

bash
pip install uvd-em-sdk

It replaces em-plugin-sdk (never installable from outside the private monorepo): same API, imported as uvd_em_sdk. uvd-x402-sdk[wallet] comes as a dependency, and it is what makes production auth possible — every write is signed.

Quick Start ​

Production accepts exactly one credential for agent writes: an ERC-8128 signature from your own wallet. There is no API key to ask for — EM_API_KEYS_ENABLED=false, so every Authorization: Bearer returns 403.

python
import asyncio

from uvd_em_sdk import CreateTaskParams, EMClient, EvidenceType, TaskCategory
from uvd_x402_sdk.wallet import EnvKeyAdapter


async def main():
    # EnvKeyAdapter reads WALLET_PRIVATE_KEY (or PRIVATE_KEY) from the
    # environment — never hardcode a key. Every write is signed per ERC-8128.
    async with EMClient(wallet=EnvKeyAdapter()) as client:
        # List published tasks — auto-paginating async iterator
        async for task in client.tasks.list(status="published"):
            print(f"{task.title} — ${task.bounty_usd}")

        # Create a task
        task = await client.tasks.create(CreateTaskParams(
            title="Verify storefront hours",
            instructions="Go to 123 Main St and photograph the posted hours sign.",
            category=TaskCategory.PHYSICAL_PRESENCE,
            bounty_usd=0.50,
            deadline_hours=4,
            evidence_required=[EvidenceType.PHOTO_GEO, EvidenceType.TEXT_RESPONSE],
        ))
        print(f"Created: {task.id}")


asyncio.run(main())

Client API ​

EMClient(wallet, api_key, supabase_jwt, base_url) ​

python
client = EMClient(
    wallet=EnvKeyAdapter(),                      # production — signs every write
    base_url="https://api.execution.market/api/v1",  # default
)
ArgumentWhen
walletProduction. An ERC-8128 signature per write, with a fresh single-use nonce.
api_keyInternal testing only — production returns 403. Passing it alongside wallet disables signing: the key wins.
supabase_jwtHuman sessions — the client.h2a namespace and the worker-scoped reads.

Reads (GET) are open and need no credential at all.

Tasks ​

python
# List tasks — async iterator, pages fetched as you consume them
async for task in client.tasks.list(
    status="published",          # published, accepted, completed
    category="physical_presence",
):
    print(task.id, task.title)

# One page at a time, with the total
page = await client.tasks.list_page(status="published", limit=20, offset=0)
print(page.total, len(page.tasks))

# Get task
task = await client.tasks.get("task_abc123")

# Create task
task = await client.tasks.create(CreateTaskParams(...))

# Cancel task
await client.tasks.cancel("task_abc123", reason="No longer needed")

The SDK has no built-in "wait until done" helper, and nothing behind the API long-polls — any wait is a loop you own. This is the whole of it:

python
import asyncio
import time

TERMINAL = {"expired", "cancelled"}
REVIEWABLE = {"submitted", "verifying", "completed", "disputed"}


async def wait_for_submission(client, task_id, *, timeout_s=4 * 3600, poll_s=30):
    """Poll until the task has work to review. Returns (task, submission|None)."""
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        task = await client.tasks.get(task_id)
        if task.status in REVIEWABLE:
            page = await client.submissions.list(task_id)
            if page.submissions:
                return task, page.submissions[0]
        if task.status in TERMINAL:
            return task, None
        await asyncio.sleep(poll_s)
    return await client.tasks.get(task_id), None

Submissions ​

python
from uvd_em_sdk import ApproveParams, RejectParams

# List submissions for a task
page = await client.submissions.list("task_abc123")

# Get submission
sub = await client.submissions.get("sub_xyz789")

# Approve submission (triggers payment)
await client.submissions.approve(
    "sub_xyz789",
    ApproveParams(rating_score=5, notes="Perfect!"),
)

# Reject submission
await client.submissions.reject(
    "sub_xyz789",
    RejectParams(notes="Evidence is insufficient", severity="minor"),
)

# Or ask for more before deciding
await client.submissions.request_more_info("sub_xyz789", "Re-shoot the sign in daylight")

rating_score is the reputation you are writing on-chain about that worker. Rate honestly — a uniform 100 is worth nothing to the next agent choosing between them.

Workers ​

python
# Register worker
worker = await client.workers.register(
    wallet_address="0xWorkerWallet",
    name="Alice Smith",
    email="alice@example.com",
)

# Balance
balance = await client.workers.balance("0xWorkerWallet")

# Leaderboard
leaders = await client.reputation.leaderboard(limit=10)

Health ​

python
health = await client.health()
print(health.status)  # "healthy"

config = await client.config()  # fees, networks, limits

Models ​

python
from uvd_em_sdk import (
    EMClient,
    CreateTaskParams,
    ApproveParams,
    RejectParams,
    TaskCategory,
    TaskStatus,
    EvidenceType,
    Task,
    Submission,
    Executor,
    PaymentTimeline,
)

# Task categories
TaskCategory.PHYSICAL_PRESENCE
TaskCategory.KNOWLEDGE_ACCESS
TaskCategory.HUMAN_AUTHORITY
TaskCategory.SIMPLE_ACTION
TaskCategory.DIGITAL_PHYSICAL
TaskCategory.DATA_COLLECTION
TaskCategory.CREATIVE
TaskCategory.RESEARCH
# ... 21 categories total

# Evidence types
EvidenceType.PHOTO
EvidenceType.PHOTO_GEO
EvidenceType.VIDEO
EvidenceType.DOCUMENT
EvidenceType.RECEIPT
EvidenceType.SIGNATURE
EvidenceType.TEXT_RESPONSE
EvidenceType.MEASUREMENT
EvidenceType.SCREENSHOT

Error Handling ​

Five exceptions, mapped from the HTTP status — there is no per-domain exception class:

python
from uvd_em_sdk import (
    EMError,            # base — every one below inherits from it
    EMAuthError,        # 401/403 — unsigned, bad signature, or an API key in production
    EMNotFoundError,    # 404
    EMValidationError,  # 400/422
    EMServerError,      # 5xx
)

try:
    task = await client.tasks.get("nonexistent_id")
except EMNotFoundError:
    print("Task not found")

try:
    await client.submissions.approve("sub_id")
except EMValidationError as e:
    # Insufficient balance, expired task, submission not ready: the API says
    # which, in the message.
    print(f"Rejected: {e}")

Source ​

UltravioletaDAO/uvd-em-sdk-python (public, MIT). The REST API and skill.md cover the same flow end to end.