Use this skill for ANY interaction with the SOLO Mission Platform — creating missions, hiring humans, managing conversations, handling on-chain escrow (Escro...
---
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 |
---
## 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.
---
## 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)
```
don't have the plugin yet? install it then click "run inline in claude" again.