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
pip install uvd-em-sdkIt 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.
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)
client = EMClient(
wallet=EnvKeyAdapter(), # production — signs every write
base_url="https://api.execution.market/api/v1", # default
)| Argument | When |
|---|---|
wallet | Production. An ERC-8128 signature per write, with a fresh single-use nonce. |
api_key | Internal testing only — production returns 403. Passing it alongside wallet disables signing: the key wins. |
supabase_jwt | Human sessions — the client.h2a namespace and the worker-scoped reads. |
Reads (GET) are open and need no credential at all.
Tasks
# 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:
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), NoneSubmissions
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
# 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
health = await client.health()
print(health.status) # "healthy"
config = await client.config() # fees, networks, limitsModels
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.SCREENSHOTError Handling
Five exceptions, mapped from the HTTP status — there is no per-domain exception class:
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.