| name | polyhub_copy |
| description | Manage copy-trading tasks, view signals, positions and trades on Polyhub using an API key. |
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 with phub_)
curl must be available in the runtime environment.
jq is strongly recommended for building JSON payloads safely.
Safety rules
- Never print
POLYHUB_API_KEY in 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 -n instead of interpolating raw shell strings.
- Validation rules:
taskId must be a 24-char hex MongoDB ObjectID: ^[0-9a-fA-F]{24}$
ruleId must be non-empty.
taskIds in batch operations must all match the taskId format.
status for 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} with status
- “改跟单模式/改单笔或总仓位限制” ->
PATCH /api/v1/copy-tasks/{taskId} with taskConfig
- “看持仓/成交” ->
GET /positions or GET /trades
- “卖出某个仓位/全部仓位” ->
POST /sell or POST /sell-all
- “看跟单信号/统计” ->
GET /api/v1/copy-signals or /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=true when the user wants a full history or is looking for a previously deleted task.
- Use
includeDeleted=false when 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
taskConfig fields 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:
status
taskConfig
filteredByTag
targetUsername
tpslRules using create / 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
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.copyMode is 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 for taskConfig.fixedAmount.
- If switching to
ONE_TO_ONE, omit fixedAmount unless 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.
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-BUY signals are skipped.
taskConfig.buyLimitPerTradeMin
- Minimum allowed copied buy amount per trade.
0 effectively means no lower bound.
taskConfig.buyLimitPerTradeMax
- Maximum allowed copied buy amount per trade.
0 effectively means no upper bound.
taskConfig.maxBoughtPerCopyTask
- Caps total bought amount at task level.
0 effectively means no cap.
taskConfig.maxBoughtPerMarket
- Caps total bought amount for a single market inside the task.
0 effectively 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.
0 effectively means no filter.
taskConfig.minFillSize
Recommended clarification rules before updating:
- If the user says “改成固定金额跟单”, ask for
fixedAmount.
- If the user says “放大/缩小仓位”, clarify whether they mean
navMultiplier or fixedAmount.
- If the user says “限制单笔金额”, clarify whether they mean
buyLimitPerTradeMin, buyLimitPerTradeMax, or both.
- If the user says “限制总仓位”, clarify whether they mean
maxBoughtPerCopyTask or maxBoughtPerMarket.
- If the user says “限制价格区间”, ask for both
priceRangeMin and priceRangeMax.
- If the user says “调严格一点/调宽松一点”, clarify whether they mean
convictionLevel.
- If the user says “恢复跟单/暂停跟单/停止任务”, clarify whether they mean task
status or a taskConfig change.
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
status values for update are: RUNNING, PAUSED, STOPPED.
- Use
PAUSED when the user wants to temporarily stop following new trades.
- Use
RUNNING when the user wants to resume an active task.
- Use
STOPPED only when the user explicitly wants the task stopped rather than just paused.
- Do not use
DELETED in update payloads; deletion should use DELETE /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:
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
curl -sS --fail-with-body "${AUTH[@]}" "$BASE/api/v1/copy-tasks/$TASK_ID/positions?status=active"
Guidance:
active returns positions with remaining amount above the close threshold.
closed returns positions whose remaining amount is effectively zero.
- If
status is omitted or unknown, the backend currently returns all positions.
- Prefer
active / closed rather than open / 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) or conditionId
- 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:
marketId or conditionId
- Ask
amount only 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:
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:
status uses the same allowed values as single-task update: RUNNING, PAUSED, STOPPED.
taskConfig replaces the task config object sent in the batch request, so only send fields you intentionally want to set.
- If neither
status nor taskConfig is 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:
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:
limit must be a positive integer.
cursor must be a non-negative integer.
since and sinceCreatedAt must be RFC3339 or RFC3339Nano timestamps.
- Valid
action values 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
ready and copy_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 rules
update: array of {id, ...patchFields}
cancel: array of ruleId
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 verify taskId.
409: Task already exists (duplicate targetTrader).
422: Copy task limit exceeded.
400: Invalid payload or invalid query param such as includeDeleted.
5xx: Server error. Retry once with backoff; if still failing, report response body.