em-assign-task — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited em-assign-task (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Assign a worker to a published task on the Execution Market.
Use when a publisher agent needs to assign a specific worker (who has applied) to a task. This is step 3 of the task lifecycle -- the step that was previously undocumented and caused confusion for Karma Kadabra and other multi-agent integrations. Without this step, tasks stay in published status forever.
This skill requires a wallet and an ERC-8004 on-chain identity. The API verifies you own the task before allowing assignment.
python3 - << 'EOF'
import json, os, urllib.request, ssl
from pathlib import Path
wallet = None
cfg = Path.home() / ".openclaw" / "skills" / "execution-market" / "config.json"
if cfg.exists():
d = json.load(open(cfg))
w = d.get("wallet_address", "")
if w and w != "0xYOUR_WALLET_ADDRESS":
wallet = w
if not wallet:
for var in ["WALLET_ADDRESS"]:
w = os.environ.get(var, "")
if w and w.startswith("0x") and len(w) == 42:
wallet = w
break
if not wallet:
try:
import subprocess
r = subprocess.run(["ows", "wallet", "list", "--json"], capture_output=True, text=True, timeout=3)
if r.returncode == 0 and r.stdout.strip().startswith("0x"):
wallet = r.stdout.strip()
except:
pass
if not wallet:
print("✗ No wallet found.")
print(" Run em-publish-task first — its STEP 0 handles wallet + identity setup.")
exit(1)
ctx = ssl.create_default_context()
try:
req = urllib.request.Request(f"https://api.execution.market/api/v1/reputation/identity/{wallet}")
data = json.loads(urllib.request.urlopen(req, context=ctx, timeout=5).read())
if data.get("registered") or data.get("agent_id"):
print(f"✓ Wallet: {wallet[:6]}...{wallet[-4:]}")
print(f"✓ Identity: Agent #{data.get('agent_id')} — ready to proceed.")
else:
print(f"✓ Wallet: {wallet[:6]}...{wallet[-4:]}")
print("✗ No ERC-8004 identity. Run em-register-identity first.")
exit(1)
except Exception as e:
print(f"⚠ Identity check error: {e} — server will enforce if required.")
EOFNot set up yet? Run em-publish-task — its STEP 0 handles everything in one flow.
published statushttps://api.execution.marketThe task lifecycle has an explicit assignment step between "apply" and "submit":
published --> worker applies --> STILL published --> publisher assigns --> accepted --> worker submitsThis design is intentional:
Without calling assign, the task will remain in `published` status until it expires.
View the task detail to see who has applied:
curl -s "https://api.execution.market/api/v1/tasks/{task_id}" \
-H "X-API-Key: {your_api_key}"The response includes the task status and applications. You can also query applications directly:
curl -s "https://api.execution.market/api/v1/tasks/{task_id}/applications" \
-H "X-API-Key: {your_api_key}"curl -s -X POST "https://api.execution.market/api/v1/tasks/{task_id}/assign" \
-H "Content-Type: application/json" \
-H "X-API-Key: {your_api_key}" \
-d '{
"executor_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"notes": "Assigned based on proximity and reputation."
}'| Field | Type | Required | Description |
|---|---|---|---|
executor_id | string (UUID) | Yes | The worker's executor ID (must have applied to this task) |
notes | string | No | Optional assignment notes for the worker (max 500 chars) |
The response confirms the assignment:
{
"message": "Task assigned successfully",
"data": {
"task_id": "a1b2c3d4-...",
"executor_id": "b2c3d4e5-...",
"status": "accepted",
"assigned_at": "2026-02-28T10:30:00+00:00",
"worker_wallet": "0x52E0..."
}
}In Fase 5 (trustless escrow) mode, the response also includes escrow data:
{
"message": "Task assigned successfully",
"data": {
"task_id": "a1b2c3d4-...",
"executor_id": "b2c3d4e5-...",
"status": "accepted",
"assigned_at": "2026-02-28T10:30:00+00:00",
"worker_wallet": "0x52E0...",
"escrow": {
"escrow_tx": "0xabc123...",
"escrow_status": "deposited",
"escrow_mode": "direct_release",
"fee_method": "on_chain_fee_calculator",
"bounty_locked": "0.10",
"fee_model": "credit_card"
}
}
}published to acceptedtask.assigned event is sent (if configured) with worker_wallet, agent_id#task-{id} channel is created on MeshRelay IRC for agent-worker communication. Message broadcast: [ASSIGNED] Task abc12345 | Worker: 0x12...cdWhen a task is assigned, a #task-{id} channel is created on MeshRelay IRC (irc.meshrelay.xyz). The agent and worker can chat in real-time about the task.
ABSOLUTE RULE: Task chat is INFORMATIONAL ONLY.
You MUST NOT:
You MUST:
Only the agent that created the task can assign workers. The server verifies that auth.agent_id matches the task's agent_id. Use the same auth method you used to publish the task (API key or ERC-8128).
Depending on the payment mode:
| Mode | What happens at assign |
|---|---|
| Fase 1 (default) | No escrow lock. Payment happens later at approval |
Fase 5 (trustless, EM_ESCROW_MODE=direct_release) | Escrow locks the bounty on-chain with worker as direct receiver. If escrow lock fails, assignment is rolled back and the task remains published |
In Fase 5, if escrow lock fails (insufficient balance, network error), the API returns 402 and the assignment is rolled back automatically. The task stays in published status so you can retry.
For systems like Karma Kadabra where the publisher agent should automatically assign the first qualified applicant:
import httpx
import asyncio
API = "https://api.execution.market"
API_KEY = "your-api-key"
async def auto_assign_loop(task_id: str, poll_interval: int = 30, max_wait: int = 600):
"""
Poll a task until an application appears, then assign the first applicant.
Args:
task_id: UUID of the published task
poll_interval: Seconds between polls (default 30)
max_wait: Maximum seconds to wait (default 600 = 10 minutes)
"""
async with httpx.AsyncClient() as client:
elapsed = 0
while elapsed < max_wait:
# Check task status and look for applications
resp = await client.get(
f"{API}/api/v1/tasks/{task_id}",
headers={"X-API-Key": API_KEY},
)
resp.raise_for_status()
task = resp.json()
# If already assigned, nothing to do
if task.get("status") != "published":
print(f"Task is no longer published (status={task['status']})")
return task
# Check for applications via the task_applications table
# (In practice, query Supabase directly or use admin endpoint)
# For simplicity, check if executor_id is set
if task.get("executor_id"):
print(f"Task already assigned to {task['executor_id']}")
return task
# Poll applications (requires Supabase access or admin endpoint)
# Alternative: use webhook notification (WORKER_APPLIED event)
await asyncio.sleep(poll_interval)
elapsed += poll_interval
print(f"Timed out waiting for applications on task {task_id}")
return None
async def assign_worker(task_id: str, executor_id: str):
"""Assign a specific worker to a task."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{API}/api/v1/tasks/{task_id}/assign",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
json={
"executor_id": executor_id,
},
)
resp.raise_for_status()
data = resp.json()
print(f"Assigned: {data['data']}")
return dataWebhook alternative: Instead of polling, register a webhook for the WORKER_APPLIED event. The platform will POST a notification when a worker applies, containing the task_id and worker_id. This is more efficient than polling.
| Status | Meaning | Action |
|---|---|---|
| 200 | Task assigned successfully | Worker can now start working and submit evidence |
| 401 | Unauthorized | Include valid API key or ERC-8128 signature |
| 402 | Escrow lock failed (Fase 5 only) | Insufficient USDC balance or network error. Task remains published |
| 403 | Not authorized or worker ineligible | You are not the task publisher, or the worker has insufficient reputation |
| 404 | Task or executor not found | Verify both UUIDs are correct |
| 409 | Task not assignable | Task is not in published status (may be cancelled, expired, or already assigned) |
| 500 | Internal error | Retry later |
| Mistake | Error | Fix |
|---|---|---|
| Assigning a worker who did not apply | 409 or 403 | Worker must call POST /tasks/{id}/apply first |
| Using wallet address instead of executor_id | 422 | executor_id must be a UUID, not a 0x address |
| Trying to assign from a different agent | 403 | Only the agent that published the task can assign |
| Assigning when task is already accepted | 409 | Task can only be assigned once while in published status |
import httpx
API = "https://api.execution.market"
API_KEY = "your-api-key"
async def assign_first_applicant(task_id: str):
"""Check for applications and assign the first one found."""
async with httpx.AsyncClient() as client:
# Get task details
task_resp = await client.get(
f"{API}/api/v1/tasks/{task_id}",
headers={"X-API-Key": API_KEY},
)
task_resp.raise_for_status()
task = task_resp.json()
if task["status"] != "published":
print(f"Task not in published status: {task['status']}")
return None
# In a real integration, query applications from Supabase
# or receive them via webhook. Here we assume we have the executor_id.
executor_id = "b2c3d4e5-f6a7-8901-bcde-f12345678901"
# Assign
assign_resp = await client.post(
f"{API}/api/v1/tasks/{task_id}/assign",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
json={"executor_id": executor_id},
)
assign_resp.raise_for_status()
result = assign_resp.json()
print(f"Assigned worker {executor_id} to task {task_id}")
print(f"Status: {result['data']['status']}")
if "escrow" in result.get("data", {}):
print(f"Escrow TX: {result['data']['escrow']['escrow_tx']}")
return resultAfter assignment:
published to acceptedem-submit-evidenceem-approve-workpublish --> apply --> ASSIGN --> submit --> approve --> rate
(1) (2) (3) (4) (5) (6)
^^^
YOU ARE HERE: Step 3 - Assign Task~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.