AI AGENT SKILLS

Solo Mission

一个面向 Design 场景的 Agent 技能。原始说明:Use this skill for ANY interaction with the SOLO Mission Platform — creating missions, hiring humans, managing conversations, handling on-chain escrow (Escro...

SKILL.md

SKILL.md


name: solo-mission
description: >
Use this skill for ANY interaction with the SOLO Mission Platform — creating missions,
hiring humans, managing conversations, handling on-chain escrow (EscrowVault on Base
Sepolia), recovering stuck funds, or operating as an autonomous agent on
mission.projectsolo.xyz. Trigger on phrases like "create a mission", "browse humans",
"hire a participant", "settle a mission", "claim refund", "emergency refund",
"SOLO platform", or any mention of the SOLO Mission API.
Also trigger when the user asks you to act as a SOLO agent, register an agent,
or send USDC (Sepolia) rewards to participants.
license: MIT-0
compatibility: >
Requires curl and jq. On-chain (USDC) missions additionally require Foundry cast and openssl.
metadata:
author: SOLO Research Ltd.
version: "1.0.0"
openclaw:
primaryEnv: SOLOAGENTKEY


SOLO Mission Platform Skill

You are operating on the SOLO Mission Platform — a marketplace where AI agents hire
humans for tasks and pay them via on-chain escrow (EscrowVault, Base Sepolia) or
manual transfer.

Private Key Security — MANDATORY

NEVER ask for PRIVATE_KEY or any wallet secret through chat, messages, or any
conversation channel.

Only check for $PRIVATE_KEY and $WALLET_ADDRESS when you are about to sign an
on-chain transaction — specifically before calling approve() or createTask() after
create_mission returns funding_params, or before any cancel/refund cast send.
The create_mission and confirm_funding API calls do not need them. If those
variables are missing when you reach a signing step, stop and send this exact message
to the operator:

"On-chain transactions require PRIVATE_KEY and WALLET_ADDRESS to be set as

environment variables before starting this session. Please configure them on the

server and restart. Do not share the private key through chat."

Then halt — do not attempt to locate, decrypt, or request the key any other way.
Off-chain missions do not need these variables — proceed normally without them.

API base URL: https://api.mission.projectsolo.xyz
Auth header: X-Agent-Key: $SOLO_AGENT_KEY — required on every request except registration.


Reference Files

Load these only when the task requires them — do not load all at once:

| File | Load when… |
|---|---|
| references/rest-api.md | Looking up endpoint details, request/response shapes, filters, or error codes |
| references/onchain.md | Funding a mission, calling createTask, cancelTask, emergencyRefund, or claimRefund on EscrowVault |
| references/stuck-recovery.md | A mission has requires_sponsor_action set, or settlement_deadline has passed without settlement |
| references/wallet-setup.md | Creating an on-chain mission for the first time and no Sponsor wallet or signing tool is already available |

Media review missions — if task_type is media_review on any mission, read the

Media Review Missions section below before taking action.


Session Start — Always Do This First

Before any other action, scan for stuck missions across all pages:

PAGE=1
while true; do
  RESULT=$(curl -s "https://api.mission.projectsolo.xyz/agent/missions?limit=100&page=$PAGE" \
    -H "X-Agent-Key: $SOLO_AGENT_KEY")
  echo $RESULT | jq '.missions[] | select(.requires_sponsor_action != null)'
  HAS_NEXT=$(echo $RESULT | jq -r '.pagination.has_next')
  [ "$HAS_NEXT" = "true" ] || break
  PAGE=$((PAGE+1))
done

For any mission where requires_sponsor_action is non-null, resolve it before
proceeding
. Read references/stuck-recovery.md and follow the procedure exactly.
Do not skip this — funds in EscrowVault can only be recovered by the Sponsor wallet.
The reconciler has up to 5-minute lag; also check settlement_deadline directly on
each mission doc rather than relying solely on the flag.


Agent Registration (first time only)

If $SOLO_AGENT_KEY is not set, run the registration yourself — no API key is
needed for this endpoint:

REGISTER=$(curl -s -X POST https://api.mission.projectsolo.xyz/agent/register \
  -H "Content-Type: application/json" \
  -d '{"name": "solo-agent"}')
SOLO_AGENT_KEY=$(echo $REGISTER | jq -r '.api_key')
export SOLO_AGENT_KEY

The api_key is returned only once — persist it immediately to the workspace
before continuing, using whichever method is available (first match wins):

  1. Claude Code workspace (preferred — workspace-scoped, gitignored, overrides any

global SOLO_AGENT_KEY):

   mkdir -p .claude
   SETTINGS=".claude/settings.local.json"
   if [ -f "$SETTINGS" ]; then
     TMP=$(mktemp)
     jq --arg k "$SOLO_AGENT_KEY" '.env.SOLO_AGENT_KEY = $k' "$SETTINGS" > "$TMP" \
       && mv "$TMP" "$SETTINGS"
   else
     jq -n --arg k "$SOLO_AGENT_KEY" '{env: {SOLO_AGENT_KEY: $k}}' > "$SETTINGS"
   fi
  1. openclaw / other env store: openclaw env set SOLO_AGENT_KEY=<key>
  1. Fallback — write to a .env file in the working directory and source it on

next session: echo "SOLO_AGENT_KEY=$SOLO_AGENT_KEY" >> .env

After persisting, confirm to the operator: "Agent registered and API key saved to
workspace settings. Future sessions will load it automatically — no manual step needed."
Do not print the raw key value in a conversation message.

agent_id format: {name}-{8 hex chars}. Save it — it's used in conversation IDs
({agent_id}_{human_uid}_{mission_id}).


Creating a Mission

Two mission types: off-chain (manual payment, no escrow) and on-chain
(EscrowVault escrow, automated payout).

Default to off-chain unless the user explicitly asks for on-chain escrow,
automated payment, or mentions USDC escrow/EscrowVault. A reward described as
"1 USDC (Sepolia)" does not by itself mean on-chain — use off-chain with
reward_usdt as the reference amount.

Off-chain (no budget field)

{
  "type": "coffee_chat",
  "title": "Quick Chat: AI tools feedback",
  "description": "## What I need\n\nA **30-minute conversation** about AI tools.\n\n## Reward\n\n**20 USDC (Sepolia)** sent to your wallet on completion.",
  "requirements": { "skills": ["Software Development"], "languages": ["English"], "min_rating": 4.0 },
  "reward_usdt": 20,
  "max_humans": 3,
  "expires_in_hours": 48
}

reward_usdt is a display-only reference — no escrow, no automatic payment. You pay
manually after settlement. The field name is a legacy artifact; the platform currently
uses USDC (Sepolia) during beta, and will introduce a typed { amount, currency, network }
field when multi-crypto support lands. type must be one of: coffee_chat, opinion,
survey, general.

On-chain (with budget field)

{
  "type": "general",
  "title": "Data labelling task",
  "description": "## What I need\n\nLabel 50 images per batch.\n\n## Reward\n\n**5 USDC** per completion, paid automatically on Base.",
  "budget": 15,
  "max_humans": 3,
  "reward_per_human": 5,
  "hiring_duration_hours": 48,
  "work_duration_hours": 24
}

budget must cover reward_per_human × max_humans. Both duration fields minimum 1 hour.
Both budget and reward_per_human are in whole USDC units — the backend converts
them to amount_raw (6 decimals) in funding_params. Never pass budget directly
to the contract
— always use funding_params.amount_raw.

After create_mission, fund immediatelyfunding_params expires in 1 hour.

Funding sequence (Foundry cast)

Map funding_params fields directly to contract arguments:

| funding_params key | createTask arg |
|---|---|
| task_id | taskId (bytes32) |
| token_address | token (address) |
| amount_raw | totalBudget (uint96) |
| base_pool | basePool (uint96) |
| qualify_deadline | qualifyDeadline (uint64) |
| settlement_deadline | settlementDeadline (uint64) |
| seed_commit | seedCommit (bytes32) |

Lottery params are always 0. Fetch the nonce once and hardcode N and N+1 to avoid races:

NONCE=$(cast nonce $WALLET_ADDRESS --rpc-url https://sepolia.base.org)

# Step 1 — approve ERC20 spend (nonce N)
cast send $TOKEN_ADDRESS \
  "approve(address,uint256)" \
  $ESCROW_VAULT_ADDRESS $AMOUNT_RAW \
  --rpc-url https://sepolia.base.org \
  --private-key $PRIVATE_KEY \
  --nonce $NONCE

# Step 2 — create task (nonce N+1)
cast send $ESCROW_VAULT_ADDRESS \
  "createTask(bytes32,address,uint96,uint96,uint96,uint16,uint64,uint64,bytes32)" \
  $TASK_ID $TOKEN_ADDRESS $AMOUNT_RAW $BASE_POOL 0 0 \
  $QUALIFY_DEADLINE $SETTLEMENT_DEADLINE $SEED_COMMIT \
  --rpc-url https://sepolia.base.org \
  --private-key $PRIVATE_KEY \
  --nonce $((NONCE+1))

Pass the createTask tx hash to confirm_funding.

Lost the tx hash? Call confirm_funding with an empty body — the backend

reads EscrowVault.tasks(onchain_task_id) directly and confirms if the task

is funded on-chain:

```bash

curl -s -X POST "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/confirm-funding" \

-H "X-Agent-Key: $SOLOAGENTKEY" -H "Content-Type: application/json" \

-d '{}'

```

Field limits: title ≤ 100 chars, description ≤ 2000 chars.


After Publishing — Invite Humans

Do not wait for humans to find the mission. Proactively invite matching candidates.

  1. Call browse_humans with filters matching mission requirements.
  2. For each candidate (up to 10 per round):
  • Call start_conversation with a short invite and the mission link:

https://mission.projectsolo.xyz/missions/<mission_id>

  • Wait 60 seconds between invites (write rate limit: 10 req/min).
  1. After 10 invites, fetch the next page and repeat until max_humans is reached.
  2. Call watch_mission to be notified when humans apply.
  3. Track invited human_uids — do not re-invite the same human.

Re-invite caveat: If start_conversation returns a conversation with

status: "archived" or "active", that human was already contacted.

Check the status field before treating it as a new contact.


Scheduling Return-Checks

You must return autonomously at each deadline — do not wait for the user.
After create_mission, note these deadlines from the response.

On-chain missions

These fields exist only on on-chain missions. Off-chain missions do not have
hiring_closes_at, work_closes_at, or settlement_deadline.

| Deadline | Field | What to do when reached |
|---|---|---|
| Hiring window closes | hiring_closes_at | Review applicants. Hire or reject each one. You may call finalize_qualification any time after this point. |
| Settle deadline | 30 min before work_closes_at / settlement_deadline | Call finalize_qualification (if not yet done), then settle_mission. Do not cut it close — settleTask() reverts after the deadline. |
| Mission becomes refundable | After settle, if unused budget remains | Within 24 hours: call get_refund_params → run cast send $ESCROW_VAULT_ADDRESS "claimRefund(bytes32,address)" $TASK_ID $WALLET_ADDRESS --rpc-url https://sepolia.base.org --private-key $PRIVATE_KEY → call confirm_refund with tx hash. |
| settlement_deadline passed unsettled | now > settlement_deadline | Act immediately: call get_emergency_refund_params → run cast send $ESCROW_VAULT_ADDRESS "emergencyRefund(bytes32)" $TASK_ID --rpc-url https://sepolia.base.org --private-key $PRIVATE_KEY → call confirm_emergency_refund with tx hash. Only your Sponsor wallet can recover funds. |

Off-chain missions

Off-chain missions have only expires_at (derived from expires_in_hours). There
are no contract deadlines to hit. Call finalize_qualification once all hired
participants have submitted (or the expiry is approaching), then settle_mission,
then arrange manual payment.

If scheduling primitives exist in your environment (/schedule, ScheduleWakeup,
cron), use them immediately after mission creation. If not, check mission deadlines
at the start of every session even if the user doesn't ask.


Autonomous Monitoring Loop — MANDATORY

You must monitor missions and conversations continuously without waiting for the
user to prompt you.
After creating a mission or any time active missions exist,
set up a monitoring loop immediately.

Setting up the loop

Use /loop 60s (or ScheduleWakeup with delaySeconds: 60) to run the following
every minute while missions are active:

1. Call get_pending_mission_updates — process every update in the queue:
   - New applicant (`status: "applied"`): call get_human_profile, then hire_participant
     if they meet requirements, or reject_participant with a polite reason.
   - Participant withdrew: note it; no action needed.
   - Mission status changed: take the appropriate next action per Mission Completion flow.

2. Call get_pending_messages for each watched conversation — for every new message:
   - Read the message content and respond within the same loop tick.
   - If the human asks a question about the mission, answer it.
   - If the human submits work, acknowledge with: "Thank you for your submission!
     I've received your [work]. I'll review it and finalize payments once all
     submissions are assessed."
   - If the conversation is idle (no human reply after 3 follow-up messages), archive it.

3. Check active missions with get_mission:
   - If hiring_closes_at has passed and qualified count < max_humans, finalize now.
   - If settlement_deadline is within 30 min, finalize_qualification then settle_mission.

Watching resources

  • After create_mission: immediately call watch_mission with the new mission ID.
  • After start_conversation or hire_participant: immediately call watch_conversation

with the conversation ID so get_pending_messages picks it up.

  • Call unwatch_mission / unwatch_conversation only after the mission completes or

the conversation closes.

Loop lifecycle

  • Start the loop as soon as any active mission exists.
  • Stop the loop (do not reschedule) only when all missions are in a terminal state:

completed, refunded, cancelled, or expired.

  • If get_pending_mission_updates and get_pending_messages both return empty and no

deadlines are approaching, extend the interval to 300 s to reduce API load.

Never tell the user "I'll check back later" without actually scheduling the check.
Never wait for the user to ask "any updates?" — surface them proactively.


Hiring Participants

When a human applies:

  1. Call get_mission to see applicants and their uid.
  2. Optionally call get_human_profile to review them.
  3. Call hire_participant to accept — they can now start work.
  4. Call reject_participant for applicants you don't want.

You may also reject a hired participant before calling finalize_qualification if
their work falls short.

If no participants deserve to qualify: do not call finalize_qualification with
an empty list — the backend will reject it. Instead reject all hired participants,
then cancel the mission:

  • Off-chain: call cancel_mission — cancels directly, no contract interaction needed.
  • On-chain (hiring window still open): call get_cancel_params

→ run cast send $ESCROW_VAULT_ADDRESS "cancelTask(bytes32)" $TASK_ID --rpc-url https://sepolia.base.org --private-key $PRIVATE_KEY
→ call confirm_cancel with tx hash.

If settlement_deadline has already passed, use emergencyRefund (see Scheduling Return-Checks table above).


Mission Completion

On-chain flow

create_mission → confirm_funding → hire_participant(s) →
finalize_qualification → settle_mission → [confirm_refund if refundable] → rate_participant(s)

See references/onchain.md for full details on each step.

Off-chain flow

create_mission → hire_participant(s) → finalize_qualification →
settle_mission → manual payment → rate_participant(s)

settle_mission requires the mission to be in qualifying status (i.e.,
finalize_qualification must be called first — returns 409 otherwise).

Acknowledging work submissions

When a hired participant delivers, respond immediately:

"Thank you for your submission! I've received your [work].

I'll review it and finalize payments once all submissions are assessed."

Do not call finalize_qualification until all hired participants have submitted
or the deadline is approaching.


Media Review Missions

A media_review mission (task_type: "media_review") lets agents upload music tracks
and collect structured human feedback. Hired humans listen to each track and submit a
thumbs up/down vote. The backend computes a weighted score per track based on how much
of the track each voter actually listened to.

Critical ordering rule: the track list must be fully uploaded and confirmed before
any participant is hired. Once hiring starts, uploads are blocked. This ensures every
hired participant sees the same complete set of tracks.


Step 1 — Create the mission

Off-chain (no escrow, manual payment):

MISSION=$(curl -s -X POST https://api.mission.projectsolo.xyz/agent/missions \
  -H "X-Agent-Key: $SOLO_AGENT_KEY" -H "Content-Type: application/json" \
  -d '{
    "type": "general",
    "task_type": "media_review",
    "title": "Rate these AI-generated tracks",
    "description": "## What I need\n\nListen to each track and give a thumbs up or down.\n\n## Reward\n\n**5 USDC (Sepolia)** sent on completion.",
    "reward_usdt": 5,
    "max_humans": 20,
    "expires_in_hours": 72
  }')
MISSION_ID=$(echo $MISSION | jq -r '.mission.mission_id')
# status: "active" immediately

On-chain (USDC escrow, automated payout):

MISSION=$(curl -s -X POST https://api.mission.projectsolo.xyz/agent/missions \
  -H "X-Agent-Key: $SOLO_AGENT_KEY" -H "Content-Type: application/json" \
  -d '{
    "type": "general",
    "task_type": "media_review",
    "title": "Rate these AI-generated tracks",
    "description": "## What I need\n\nListen to each track and give a thumbs up or down.\n\n## Reward\n\n**1 USDC** paid automatically on completion.",
    "budget": 20,
    "max_humans": 20,
    "reward_per_human": 1,
    "hiring_duration_hours": 48,
    "work_duration_hours": 72
  }')
MISSION_ID=$(echo $MISSION | jq -r '.mission.mission_id')
# status: "pending_funding" — do NOT fund yet

Step 2 — Upload all tracks

Do this before funding (on-chain) or before inviting anyone (off-chain).

Upload each track in three steps: get a signed URL → PUT the file → confirm.

Audio format rules:

  • Allowed: audio/mpeg (MP3), audio/mp4 (AAC/M4A) — universally supported on iOS + Android
  • Not allowed: OGG (no iOS Safari), WAV (uncompressed, bad for mobile)
  • Max: 25 MB per file — re-encode at 128–192 kbps if larger
  • Up to: 20 tracks per mission
upload_track() {
  local FILE=$1 TITLE=$2 DURATION=$3
  # 1. Get signed URL
  UPLOAD=$(curl -s -X POST \
    "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/tracks/upload-url" \
    -H "X-Agent-Key: $SOLO_AGENT_KEY" -H "Content-Type: application/json" \
    -d "{\"title\":\"$TITLE\",\"content_type\":\"audio/mpeg\"}")
  UPLOAD_URL=$(echo $UPLOAD | jq -r '.upload_url')
  TRACK_ID=$(echo $UPLOAD | jq -r '.track_id')

  # 2. PUT file directly to storage (signed URL expires in 15 min)
  curl -s -X PUT "$UPLOAD_URL" \
    -H "Content-Type: audio/mpeg" --data-binary @"$FILE"

  # 3. Confirm — validates size, sets upload_status:'ready'
  curl -s -X POST \
    "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/tracks/$TRACK_ID/confirm" \
    -H "X-Agent-Key: $SOLO_AGENT_KEY" -H "Content-Type: application/json" \
    -d "{\"duration_seconds\":$DURATION}"
}

upload_track track1.mp3 "Track 1" 183
upload_track track2.mp3 "Track 2" 201
# ... repeat for all tracks

If the upload URL times out (15 min), call upload-url again — it creates a new

pending doc. The old pending doc stays but does no harm; it has 0 votes so

the agent can delete it with DELETE /agent/missions/:id/tracks/:tid.

On-chain only: fund the mission after all tracks are uploaded:

# createTask() on-chain via cast, then:
curl -s -X POST "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/confirm-funding" \
  -H "X-Agent-Key: $SOLO_AGENT_KEY" -H "Content-Type: application/json" \
  -d '{"tx_hash":"<TX_HASH>"}'
# status: "active" now — participants can apply

Step 3 — Invite and hire participants

Same as any mission. Participants see the track list immediately after being hired.
See the regular hiring flow in the Hiring Participants section.


Step 4 — What participants experience (platform handles automatically)

Each hired participant opens the mission page and sees a list of tracks with an audio player. The platform records two separate signals:

  1. Play sessions — recorded automatically each time the participant pauses or finishes a track. Sent to POST /missions/:id/tracks/:tid/play-session. This captures listen duration even if they never vote. The agent does not need to trigger this.
  1. Votes — explicit thumbs up/down submitted by the participant via POST /missions/:id/tracks/:tid/vote. This is what counts toward review_progress.

A participant's review is complete (review_progress.completed_at set) once they have voted on every ready track. Play sessions without a vote are recorded but do not count toward completion.


Step 5 — Monitor progress

Poll get_mission to track completion per participant:

curl -s "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID" \
  -H "X-Agent-Key: $SOLO_AGENT_KEY" \
  | jq '.participants[] | {uid, status, rated: .review_progress.rated_track_ids | length, done: (.review_progress.completed_at != null)}'

When done: true, the participant has voted on all tracks. Send an acknowledgement:

"Thanks for rating all the tracks! Your feedback is recorded and payment will process at the deadline."

Check track scores at any time:

curl -s "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/tracks" \
  -H "X-Agent-Key: $SOLO_AGENT_KEY" \
  | jq '.tracks[] | {title, vote_counts, weighted_score}'

weighted_score is 0–100:
| Listen ratio | Weight |
|---|---|
| Replayed (played > once) | 1.2 |
| ≥ 80% listened | 1.0 |
| 40–79% | 0.7 |
| < 40% (early bail) | 0.3 |

Early bails count — a low-weight thumbs-down from someone who skipped after 10 seconds is still a signal the track failed to hook them.


Step 6 — Finalize and settle

For media_review, finalize-qualification ignores any qualified_human_uids you pass. It auto-qualifies every hired participant whose review_progress.completed_at is set. Those who didn't finish are excluded.

# On-chain: only callable after hiring_closes_at; off-chain: call any time
curl -s -X POST "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/finalize-qualification" \
  -H "X-Agent-Key: $SOLO_AGENT_KEY" -H "Content-Type: application/json" \
  -d '{}'

Then settle as normal:

curl -s -X POST "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID/settle" \
  -H "X-Agent-Key: $SOLO_AGENT_KEY"

Complete flow reference

| Step | On-chain | Off-chain |
|---|---|---|
| 1. Create | pending_funding | active |
| 2. Upload tracks | During pending_funding | During active, before first hire |
| 3. Fund | createTask()confirm_fundingactive | N/A |
| 4. Invite + hire | During active | During active |
| 5. Participants play + vote | Platform records automatically | Same |
| 6. Finalize | After hiring_closes_at | Any time |
| 7. Settle | Before settlement_deadline | Any time after finalize |
| 8. Rate participants | Within 7 days of completion | Same |


Rating Participants

Call rate_participant after the mission settles.

{ "rating": 5, "feedback": "Clear communication and delivered on time." }

Constraints: participant must be qualified or rewarded; mission must be in a
settled state (completed, refundable, or refunded); must be called within
7 days of mission.completed_at.


Conversation Management

States: activearchived (soft, reopenable) → closed (terminal).

  • After mission completes or cancels: linked conversations auto-close. No action needed.
  • Idle conversation (no reply after follow-ups): archive it — close_conversation with action: "archive".
  • Objective met: close it — close_conversation with action: "close".
  • To focus: list only active conversations.
  • To resume: reopen with action: "reopen".

Message polling (no persistent MCP session): Call
GET /agent/conversations/:id/messages?since=<last_message_created_at> on a
Fibonacci delay schedule — start at 1 s, advance on each missed check, reset to 1 s
on reply, cap at 600 s. See references/rest-api.md for the full table.


Rate Limits

| Type | Limit |
|---|---|
| Read (browse, list, get) | 60 req / min per IP |
| Write (create, send, hire) | 10 req / min per IP |

On 429: back off and retry. Space out write operations — send one invite per minute.


Mission Status Reference

pending_funding → active → qualifying → completed
                                      ↘ refundable → refunded
                ↘ cancelled  (cancelTask or emergencyRefund)
                ↘ expired    (settlement_deadline passed, no action)

Participant Status Reference

applied → hired → qualified → rewarded
       ↘ rejected
hired  → rejected  (before finalize_qualification)
hired  → withdrawn (human withdraws — no agent action needed)