Polyhub Copy Skill
Version: v0.3.3
When to use
Use this skill when the user asks about:
- Listing, creating, updating or deleting copy-trading tasks
- Viewing copy task positions or trades
- Selling positions (single or all)
- Batch updating or deleting copy tasks
- Viewing total PnL across copy tasks
- Viewing copy signals and signal stats
- Managing take-profit / stop-loss (TPSL) rules
Requirements
POLYHUB_API_BASE_URL— Polyhub API server URL (e.g.https://api.polyhub.example.com)POLYHUB_API_KEY— API key (must start withphub_)curlmust be available in the runtime environment.jqis strongly recommended for building JSON payloads safely.
Safety rules
- Never print
POLYHUB_API_KEYin the output. - Treat all IDs and user-provided JSON fields as untrusted input.
- For write actions (
POST/PATCH/DELETE), repeat the action summary and wait for explicit user confirmation before calling the API. - Prefer building JSON with
jq -ninstead of interpolating raw shell strings. - Validation rules:
taskIdmust be a 24-char hex MongoDB ObjectID:^[0-9a-fA-F]{24}$ruleIdmust be non-empty.taskIdsin batch operations must all match thetaskIdformat.statusfor positions should only be passed when the user explicitly requests a filter.
Tools
Use the bash tool to call the API with curl.
Fast Path
For common intents, map user requests like this:
- “列出任务” ->
GET /api/v1/copy-tasks - “暂停/恢复/停止任务” ->
PATCH /api/v1/copy-tasks/{taskId}withstatus - “改跟单模式/改单笔或总仓位限制” ->
PATCH /api/v1/copy-tasks/{taskId}withtaskConfig - “看持仓/成交” ->
GET /positionsorGET /trades - “卖出某个仓位/全部仓位” ->
POST /sellorPOST /sell-all - “看跟单信号/统计” ->
GET /api/v1/copy-signalsor/stats
Curl base setup
BASE="${POLYHUB_API_BASE_URL%/}"
AUTH=(-H "Authorization: Bearer $POLYHUB_API_KEY" -H "Content-Type: application/json")
Validate helpers
if [[ ! "$TASK_ID" =~ ^[0-9a-fA-F]{24}$ ]]; then echo "Invalid taskId"; exit 2; fi
if [[ -z "${RULE_ID:-}" ]]; then echo "Invalid ruleId"; exit 2; fi
for id in "${TASK_IDS[@]}"; do
if [[ ! "$id" =~ ^[0-9a-fA-F]{24}$ ]]; then
echo "Invalid taskId in batch: $id"
exit 2
fi
done
Copy Tasks
Action: List copy tasks
GET /api/v1/copy-tasks- Query params:
includeDeleted=true|false(default: true)
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks?includeDeleted=true"
Guidance:
- Use
includeDeleted=truewhen the user wants a full history or is looking for a previously deleted task. - Use
includeDeleted=falsewhen the user only wants active/non-deleted tasks.
Action: Create copy task
POST /api/v1/copy-tasks- Required field:
targetTrader(Polymarket address of the smart money) - Optional fields from
domain.CreateCopyTaskPayload:targetUsername,taskConfig,filteredByTag,tpslRules
Before calling: ask the user for targetTrader at minimum. Summarize and confirm.
Minimum fields to ask:
- Always ask:
targetTrader - Ask only if needed:
filteredByTag,targetUsername - If the user wants custom copy behavior: ask for the relevant
taskConfigfields explicitly - If the user wants TPSL on creation: ask for
tpslRules
PAYLOAD="$(jq -n \
--arg targetTrader "0x..." \
'{targetTrader: $targetTrader}')"
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks" \
-d "$PAYLOAD"
If the user wants advanced config, build taskConfig and tpslRules explicitly instead of accepting arbitrary JSON.
Action: Get copy task
GET /api/v1/copy-tasks/{taskId}
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks/$TASK_ID"
Action: Update copy task
PATCH /api/v1/copy-tasks/{taskId}- Body: partial JSON. Common fields:
statustaskConfigfilteredByTagtargetUsernametpslRulesusingcreate/update/cancel
Before calling: validate taskId, summarize changes, and confirm.
Minimum fields to ask:
- Always ask:
taskId - Ask only the fields the user wants to change:
- Task lifecycle:
status - Copy behavior:
taskConfig - Trader label/tag filters:
targetUsername,filteredByTag - TPSL maintenance:
tpslRules
- Task lifecycle:
PAYLOAD="$(jq -n \
--arg status "PAUSED" \
'{status: $status}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Example: update taskConfig safely
PAYLOAD="$(jq -n \
--argjson buyOnly true \
--argjson maxBoughtPerMarket 50 \
'{taskConfig: {buyOnly: $buyOnly, maxBoughtPerMarket: $maxBoughtPerMarket}}')"
Example: change copy mode to ONE_TO_ONE
PAYLOAD="$(jq -n \
'{taskConfig: {copyMode: "ONE_TO_ONE"}}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Example: change copy mode to FIXED_SIZE
FIXED_SIZE should include taskConfig.fixedAmount.
PAYLOAD="$(jq -n \
--argjson fixedAmount 5 \
'{taskConfig: {copyMode: "FIXED_SIZE", fixedAmount: $fixedAmount}}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Copy mode guidance:
taskConfig.copyModeis the field that controls the task's copy mode.- Confirmed mode values in the current codebase:
ONE_TO_ONE,FIXED_SIZE. - If switching to
FIXED_SIZE, also ask fortaskConfig.fixedAmount. - If switching to
ONE_TO_ONE, omitfixedAmountunless the user explicitly wants to keep it stored.
TaskConfig field guidance
Use these fields when the user wants to change task-level copy behavior. Prefer changing only the fields the user explicitly asked for.
Common fields:
taskConfig.copyMode- Controls copy sizing mode.
- Confirmed values:
ONE_TO_ONE,FIXED_SIZE.
taskConfig.fixedAmount- Used when
copyMode=FIXED_SIZE. - Represents a fixed notional amount for copied orders.
- Used when
taskConfig.navMultiplier- Multiplies the computed order size.
- Current default in backend user config is
1.
taskConfig.positionUtilization- Position utilization / max amount per copy-trade style control.
- Backend default is
0, which means no filter.
taskConfig.buyOnly- If
true, non-BUYsignals are skipped.
- If
taskConfig.buyLimitPerTradeMin- Minimum allowed copied buy amount per trade.
0effectively means no lower bound.
taskConfig.buyLimitPerTradeMax- Maximum allowed copied buy amount per trade.
0effectively means no upper bound.
taskConfig.maxBoughtPerCopyTask- Caps total bought amount at task level.
0effectively means no cap.
taskConfig.maxBoughtPerMarket- Caps total bought amount for a single market inside the task.
0effectively means no cap.
taskConfig.priceRangeMin- Minimum allowed execution price.
- Default from user config is typically
0.01.
taskConfig.priceRangeMax- Maximum allowed execution price.
- Default from user config is typically
0.99.
taskConfig.minLiquidity- Minimum market liquidity filter.
0effectively means no filter.
taskConfig.minFillSize- Minimum fill shares filter.
0effectively means no filter.
taskConfig.maxPriceDeviation- Maximum allowed price deviation percentage.
taskConfig.expiryHours- Order expiry configuration for copied orders.
- Backend default from user config is typically
0.
taskConfig.convictionLevel- Conviction-based filter level.
- Confirmed values:
OFF,LOOSE,STANDARD,STRICT. - Backend default is
STANDARD.
Recommended clarification rules before updating:
- If the user says “改成固定金额跟单”, ask for
fixedAmount. - If the user says “放大/缩小仓位”, clarify whether they mean
navMultiplierorfixedAmount. - If the user says “限制单笔金额”, clarify whether they mean
buyLimitPerTradeMin,buyLimitPerTradeMax, or both. - If the user says “限制总仓位”, clarify whether they mean
maxBoughtPerCopyTaskormaxBoughtPerMarket. - If the user says “限制价格区间”, ask for both
priceRangeMinandpriceRangeMax. - If the user says “调严格一点/调宽松一点”, clarify whether they mean
convictionLevel. - If the user says “恢复跟单/暂停跟单/停止任务”, clarify whether they mean task
statusor ataskConfigchange.
Example: update navMultiplier
PAYLOAD="$(jq -n \
--argjson navMultiplier 1.5 \
'{taskConfig: {navMultiplier: $navMultiplier}}')"
Example: enable buy-only mode
PAYLOAD="$(jq -n \
--argjson buyOnly true \
'{taskConfig: {buyOnly: $buyOnly}}')"
Example: limit per-trade buy amount
PAYLOAD="$(jq -n \
--argjson min 2 \
--argjson max 10 \
'{taskConfig: {buyLimitPerTradeMin: $min, buyLimitPerTradeMax: $max}}')"
Example: limit total bought amount
PAYLOAD="$(jq -n \
--argjson maxBoughtPerCopyTask 100 \
--argjson maxBoughtPerMarket 25 \
'{taskConfig: {maxBoughtPerCopyTask: $maxBoughtPerCopyTask, maxBoughtPerMarket: $maxBoughtPerMarket}}')"
Example: restrict execution price range
PAYLOAD="$(jq -n \
--argjson min 0.1 \
--argjson max 0.9 \
'{taskConfig: {priceRangeMin: $min, priceRangeMax: $max}}')"
Example: combine multiple taskConfig changes
PAYLOAD="$(jq -n \
--argjson buyOnly true \
--argjson navMultiplier 1.2 \
--argjson maxBoughtPerMarket 20 \
'{
taskConfig: {
buyOnly: $buyOnly,
navMultiplier: $navMultiplier,
maxBoughtPerMarket: $maxBoughtPerMarket
}
}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Example: update convictionLevel
PAYLOAD="$(jq -n \
--arg convictionLevel "STRICT" \
'{taskConfig: {convictionLevel: $convictionLevel}}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Conviction level guidance:
OFF: disable conviction filter.LOOSE: lower threshold, more signals can pass.STANDARD: default mode.STRICT: higher threshold, fewer signals can pass.- If the user gives an unknown value, normalize to one of the four explicit options before calling the API.
Example: update taskConfig and status together
PAYLOAD="$(jq -n \
--arg status "RUNNING" \
--argjson buyOnly true \
'{
status: $status,
taskConfig: {
buyOnly: $buyOnly
}
}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Example: update TPSL rules safely
PAYLOAD="$(jq -n \
--arg ruleId "$RULE_ID" \
--argjson takeProfitPct 20 \
'{tpslRules: {update: [{id: $ruleId, takeProfitPct: $takeProfitPct}]}}')"
Example: update task status
PAYLOAD="$(jq -n \
--arg status "PAUSED" \
'{status: $status}')"
curl -sS --fail-with-body "${AUTH[@]}" -X PATCH "$BASE/api/v1/copy-tasks/$TASK_ID" \
-d "$PAYLOAD"
Status guidance:
- Valid task
statusvalues for update are:RUNNING,PAUSED,STOPPED. - Use
PAUSEDwhen the user wants to temporarily stop following new trades. - Use
RUNNINGwhen the user wants to resume an active task. - Use
STOPPEDonly when the user explicitly wants the task stopped rather than just paused. - Do not use
DELETEDin update payloads; deletion should useDELETE /api/v1/copy-tasks/{taskId}. - Prefer uppercase status values in payloads.
Action: Delete copy task
DELETE /api/v1/copy-tasks/{taskId}
Before calling: validate taskId and confirm.
Minimum fields to ask:
- Always ask:
taskId
curl -sS --fail-with-body "${AUTH[@]}" -X DELETE "$BASE/api/v1/copy-tasks/$TASK_ID"
Action: Get total PnL
GET /api/v1/copy-tasks/total-pnl
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks/total-pnl"
Positions & Trades
Action: List positions
GET /api/v1/copy-tasks/{taskId}/positions- Query params:
status(optional)- Confirmed values in current code:
active,closed
- Confirmed values in current code:
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks/$TASK_ID/positions?status=active"
Guidance:
activereturns positions with remaining amount above the close threshold.closedreturns positions whose remaining amount is effectively zero.- If
statusis omitted or unknown, the backend currently returns all positions. - Prefer
active/closedrather thanopen/closed.
Action: List trades
GET /api/v1/copy-tasks/{taskId}/trades
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks/$TASK_ID/trades"
Action: Sell position
POST /api/v1/copy-tasks/{taskId}/sell- Required field:
marketId(preferred) orconditionId - Optional field:
amount(partial sell; omit to sell full position)
Before calling: validate taskId, summarize (which market, amount), and confirm.
Minimum fields to ask:
- Always ask:
taskId - Always ask one of:
marketIdorconditionId - Ask
amountonly when the user wants a partial sell
PAYLOAD="$(jq -n \
--arg marketId "..." \
--argjson amount 10 \
'{marketId: $marketId, amount: $amount}')"
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks/$TASK_ID/sell" \
-d "$PAYLOAD"
Action: Sell all positions
POST /api/v1/copy-tasks/{taskId}/sell-all
Before calling: validate taskId and confirm — this sells ALL positions for the task.
Minimum fields to ask:
- Always ask:
taskId
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks/$TASK_ID/sell-all"
Batch Operations
Action: Batch update tasks
POST /api/v1/copy-tasks/batch-update- Required:
taskIds(array of task IDs) - Optional:
status(string),taskConfig(object)
Before calling: summarize which tasks and what changes, and confirm.
Minimum fields to ask:
- Always ask:
taskIds - Ask at least one of:
status,taskConfig
TASK_IDS=("64f0c7e7b8e4f8c1a1b2c3d4" "64f0c7e7b8e4f8c1a1b2c3d5")
PAYLOAD="$(jq -n \
--arg status "PAUSED" \
--argjson taskIds "$(printf '%s\n' "${TASK_IDS[@]}" | jq -R . | jq -s .)" \
'{taskIds: $taskIds, status: $status}')"
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks/batch-update" \
-d "$PAYLOAD"
Example: batch update taskConfig.copyMode
TASK_IDS=("64f0c7e7b8e4f8c1a1b2c3d4" "64f0c7e7b8e4f8c1a1b2c3d5")
PAYLOAD="$(jq -n \
--argjson taskIds "$(printf '%s\n' "${TASK_IDS[@]}" | jq -R . | jq -s .)" \
'{
taskIds: $taskIds,
taskConfig: {
copyMode: "ONE_TO_ONE"
}
}')"
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks/batch-update" \
-d "$PAYLOAD"
Batch update guidance:
statususes the same allowed values as single-task update:RUNNING,PAUSED,STOPPED.taskConfigreplaces the task config object sent in the batch request, so only send fields you intentionally want to set.- If neither
statusnortaskConfigis provided, the backend currently just returns the selected tasks. - Use batch update only when the same change should apply to all listed tasks.
Action: Batch delete tasks
POST /api/v1/copy-tasks/batch-delete- Required:
taskIds(array of task IDs)
Before calling: confirm deletion.
Minimum fields to ask:
- Always ask:
taskIds
TASK_IDS=("64f0c7e7b8e4f8c1a1b2c3d4" "64f0c7e7b8e4f8c1a1b2c3d5")
PAYLOAD="$(jq -n \
--argjson taskIds "$(printf '%s\n' "${TASK_IDS[@]}" | jq -R . | jq -s .)" \
'{taskIds: $taskIds}')"
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks/batch-delete" \
-d "$PAYLOAD"
Copy Signals
Action: List copy signals
GET /api/v1/copy-signals- Query params:
limit(int, default 50)cursor(int, offset for pagination)since(RFC3339 timestamp)sinceCreatedAt(RFC3339 timestamp)action(one of:COPIED,SKIPPED,FAILED,RECEIVED)trader(address filter)
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-signals?limit=20&action=COPIED"
Prefer composing the query from user intent. Only pass filters the user asked for.
Validation:
limitmust be a positive integer.cursormust be a non-negative integer.sinceandsinceCreatedAtmust be RFC3339 or RFC3339Nano timestamps.- Valid
actionvalues are:COPIED,SKIPPED,FAILED,RECEIVED.
Action guidance:
RECEIVED: the signal was ingested.COPIED: the copy order was placed successfully.SKIPPED: the signal was intentionally not copied because checks or limits blocked it.FAILED: the system attempted processing but failed.
Example: fetch signals since a specific time
curl -sS --fail-with-body "${AUTH[@]}" \
"$BASE/api/v1/copy-signals?since=2026-03-01T00:00:00Z&limit=50"
Example: fetch next page using cursor
curl -sS --fail-with-body "${AUTH[@]}" \
"$BASE/api/v1/copy-signals?cursor=50&limit=50"
Action: Stream copy signals
GET /api/v1/copy-signals/stream- Response type: Server-Sent Events (SSE)
curl -sS -N "${AUTH[@]}" "$BASE/api/v1/copy-signals/stream"
Stream guidance:
- Use this only when the user explicitly wants real-time signal updates.
- Expect events such as
readyandcopy_signal. - Keep the connection open; this is not a one-shot request.
Action: Get signal stats
GET /api/v1/copy-signals/stats
Returns signal counts grouped by action and by trader.
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-signals/stats"
TPSL Rules (Take-Profit / Stop-Loss)
Action: Get TPSL rule
GET /api/v1/copy-tasks/{taskId}/tpsl-rules/{ruleId}
Minimum fields to ask:
- Always ask:
taskId,ruleId
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks/$TASK_ID/tpsl-rules/$RULE_ID"
Action: Simulate TPSL rule
POST /api/v1/copy-tasks/{taskId}/tpsl-rules/{ruleId}/simulate
Minimum fields to ask:
- Always ask:
taskId,ruleId
curl -sS --fail-with-body "${AUTH[@]}" -X POST "$BASE/api/v1/copy-tasks/$TASK_ID/tpsl-rules/$RULE_ID/simulate"
TPSL inline payload guidance
When creating a task, tpslRules items may include:
sourceOrderId,sourceType- Optional entry info:
conditionId,tokenId,assetId,entryPrice,entrySize,side - Optional risk config:
takeProfitPct,takeProfitProb,stopLossPct,limitPriceOffset,orderTimeout,fallbackToMarket
When updating a task, tpslRules should be wrapped as:
create: array of new inline rulesupdate: array of{id, ...patchFields}cancel: array ofruleId
Example: create a task with one TPSL rule
PAYLOAD="$(jq -n \
--arg targetTrader "0x..." \
--arg sourceOrderId "order_123" \
--arg sourceType "copy_trade" \
--argjson takeProfitPct 20 \
--argjson stopLossPct 10 \
'{
targetTrader: $targetTrader,
tpslRules: [
{
sourceOrderId: $sourceOrderId,
sourceType: $sourceType,
takeProfitPct: $takeProfitPct,
stopLossPct: $stopLossPct
}
]
}')"
Error handling
401: API key missing/invalid/expired/disabled. Ask user to check or regenerate key.404: Task or resource not found. Ask user to verifytaskId.409: Task already exists (duplicatetargetTrader).422: Copy task limit exceeded.400: Invalid payload or invalid query param such asincludeDeleted.5xx: Server error. Retry once with backoff; if still failing, report response body.