Python SDK
The em-plugin-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
Not on PyPI yet
em-plugin-sdk is not published: the name does not exist on PyPI, and the monorepo that ships it is private, so a pip install from its git URL fails for anyone outside the organisation — as an authentication error or a 404, which reads like "the package does not exist". There is no install command for it today. Integrate against the REST API and skill.md, which cover the whole flow.
From a checkout, for contributors:
cd execution-market/em-plugin-sdk
pip install -e ".[dev]"The [wallet] extra 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 em_plugin_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 em_plugin_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 em_plugin_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 em_plugin_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
The SDK ships from the Execution Market monorepo, which is private — the package is not on PyPI and the git URL fails for anyone outside the organisation. Until it is published, the REST API is the supported integration surface, and skill.md covers the same flow end to end.