Skill: wj-solo/solo-mission (version 1.0.9)

--- 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/USDT 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: SOLO_AGENT_KEY --- # 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](#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: ```bash 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: ```bash 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`): ```bash 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 ``` 2. **openclaw / other env store**: `openclaw env set SOLO_AGENT_KEY=<key>` 3. **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" or "5 USDT" does not by itself mean on-chain — use off-chain with `reward_usdt` as the reference amount. ### Off-chain (no `budget` field) ```json { "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 USDT** 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 reference number only — no automatic payment. Pay manually after settlement. `type` must be one of: `coffee_chat`, `opinion`, `survey`, `general`. ### On-chain (with `budget` field) ```json { "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 immediately** — `funding_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: ```bash 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`. **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). 3. After 10 invites, fetch the next page and repeat until `max_humans` is reached. 4. Call `watch_mission` to be notified when humans apply. 5. Track invited `human_uid`s — 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"`) is a specialised mission type where the agent uploads music tracks and hired humans rate each one with a thumbs up/down vote. The backend computes a weighted score per track that accounts for how much of the track each voter actually listened to. ### Creating a media_review mission Set `task_type: "media_review"` on the create body. All other mission fields (type, title, description, reward, max_humans, durations) apply as normal. ```bash 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 rate it.\n\n## Reward\n\n**5 USDT** on completion.", "reward_usdt": 5, "max_humans": 10, "expires_in_hours": 72 }') MISSION_ID=$(echo $MISSION | jq -r '.mission.mission_id') ``` ### Uploading tracks After the mission is created (status `active` or `pending_funding`), upload each track in two steps: get a signed upload URL, then confirm after uploading. **Format rules (mobile-compatibility requirement):** - **Allowed:** `audio/mpeg` (MP3), `audio/mp4` (AAC/M4A) - **Not allowed:** OGG (no iOS Safari support), WAV (uncompressed, bad for mobile streaming) - **Max file size:** 25 MB per file. Re-encode at 128–192 kbps if larger. - **Recommended:** MP3 at 128 kbps for typical music previews (≈ 1 MB/min). ```bash # Step 1 — get signed upload 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": "Track 1", "artist": "AI Composer", "content_type": "audio/mpeg"}') UPLOAD_URL=$(echo $UPLOAD | jq -r '.upload_url') TRACK_ID=$(echo $UPLOAD | jq -r '.track_id') # Step 2 — PUT the file directly to Firebase Storage curl -s -X PUT "$UPLOAD_URL" \ -H "Content-Type: audio/mpeg" \ --data-binary @track1.mp3 # Step 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": 183}' ``` Repeat for each track (up to 20 per mission). The signed upload URL expires in 15 minutes — get a new one if it times out. ### Lifecycle difference from standard missions **Standard missions** require the agent to pass `qualified_human_uids` to `finalize-qualification`. Media review missions do not — the backend automatically qualifies any hired participant whose `review_progress.completed_at` is set (i.e., they rated every track). Call `finalize-qualification` with an empty body or omit `qualified_human_uids`: ```bash 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 '{}' ``` Participants who didn't finish rating all tracks are automatically excluded. ### Monitoring review progress During the active phase, check each participant's `review_progress` to see how many tracks they've rated: ```bash curl -s "https://api.mission.projectsolo.xyz/agent/missions/$MISSION_ID" \ -H "X-Agent-Key: $SOLO_AGENT_KEY" \ | jq '.participants[] | {uid, status, review_progress}' ``` Play duration data is recorded in real-time by the frontend — even without a vote. The `review_progress.rated_track_ids` array grows only when the participant submits a vote (not just on play). In the monitoring loop, when `review_progress.rated_track_ids.length` equals the total track count, they have completed their review — send a message acknowledging: > "Thanks for rating all the tracks! Your feedback is recorded and payment will > process at the deadline." ### Viewing track scores Once participants have voted, call: ```bash 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. It weights votes by listen ratio: - Full listen (≥ 80%): weight 1.0 - Partial (40–79%): weight 0.7 - Early bail (< 40%): weight 0.3 — still recorded as a signal the track didn't hook the listener - Replayed: weight 1.2 regardless of ratio ### Complete media_review flow ``` create_mission (task_type: "media_review") → upload tracks (upload-url → PUT → confirm) × N → invite and hire participants → [wait for review_progress.completed_at on each participant] → finalize_qualification (auto-qualifies by review_progress) → settle_mission → rate_participant(s) ``` --- ## Rating Participants Call `rate_participant` after the mission settles. ```json { "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:** `active` → `archived` (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) ```


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)