| name | support-skill |
| description | Triage and resolve your team's support tickets on the ACP Support Portal. Use this skill when the user asks to handle tickets, respond to a ticket, resolve or refund a user, reassign a ticket to another team, check pending tickets, or mentions support.virtuals.io, acp_live_ tokens, or the ACP agent support API. Covers the full ticket lifecycle: list, inspect, comment (optionally via email), change status (pending → in-progress → resolved/refunded/rejected), update internal notes, and reassign. All via the `support` CLI. |
ACP Support Skill
This skill lets an ACP agent manage its own team's support tickets on the ACP Support Portal (support.virtuals.io) through the support CLI. The CLI wraps the portal's Agent API — every command supports --json for machine-readable output.
Default behavior: when a user reports that a ticket needs action ("resolve FB123", "refund this user", "is the Axelrod queue backed up"), use support to do it end-to-end rather than hand-writing curl.
Setup
One-time install:
git clone https://github.com/Virtual-Protocol/support-skill
cd support-skill && npm install
Per-session auth — export the team-scoped bearer token:
export ACP_AGENT_TOKEN=acp_live_...
export ACP_BASE=https://support.virtuals.io
Tokens are team-scoped. Super-admin tokens are rejected by the API. If support me returns UNAUTHORIZED or FORBIDDEN, rotate the token.
How to Run
Run from the repo root. Always append --json for machine-readable output; errors are emitted as {"error":"...","code":"...","recovery":"..."} to stdout in --json mode and exit 1.
support <command> [subcommand] [args] --json
Commands
| Command | Purpose |
|---|
support me | Verify the token and learn which team / admin it is bound to |
support ticket list [--status] [--job-id] | Filter the team's tickets |
support ticket get <id> | Full ticket record (includes version) |
support ticket comments <id> | Read the comment thread |
support ticket audit <id> | Read the audit log |
support ticket status <id> <status> [--message] [--tx-hash] | Change status; emails the user |
support ticket comment <id> <body> [--email] | Add a comment; optionally email |
support ticket notes <id> <text> | Replace internal notes (never emailed) |
support ticket assign <id> --reason <r> [--team <t>] [--admin <u>] | Reassign; reason required |
Statuses: pending, in-progress, resolved, refunded, rejected. The last three are terminal — the API will not transition out of them, only a super-admin reopens via the web UI.
Picking a terminal status
| User intent | Status | Required fields |
|---|
| Issue fixed, nothing on-chain moved | resolved | --message (what was done) |
| Funds returned to the user on-chain | refunded | --message, --tx-hash |
| Not actionable (duplicate, out of scope, user error) | rejected | --message (explain why) |
Always pass --message on terminal transitions — the email body is the user's only visibility into why the ticket was closed.
Same-status no-op
Calling status with the value the ticket is already in returns {"success": true, "ticket": {...}, "noChange": true} — no audit entry, no email. Check noChange in the response before assuming a transition happened.
Email-sent flag
ticket comment --email only emails when the ticket's contactMethod === 'email' AND username is populated. If either is missing the comment persists but the response returns {"success": true, "emailSent": false}. Check emailSent — don't tell the user "emailed" based on the flag you passed.
Workflows
Triage — pick the next ticket
support me --json
support ticket list --status in-progress --json
support ticket list --status pending --json
support ticket get FB123 --json
Acknowledge and start work
support ticket status FB123 in-progress \
--message "Thanks — we see your report and are investigating." --json
This writes an audit entry AND emails the user (when contactMethod=email). version is read automatically from the current ticket.
Resolve without moving money
support ticket comment FB123 "The swap completed on retry — no further action needed." --email --json
support ticket status FB123 resolved --message "Closed — swap completed on retry." --json
Refund with on-chain proof
Execute the refund transaction out-of-band first, then:
support ticket status FB999 refunded \
--message "Refund sent on-chain. Allow a few minutes for confirmation." \
--tx-hash 0xabc123... --json
The tx-hash renders in the user's refund email. Do NOT attempt ticket comment --email after a refund — terminal tickets reject outbound email.
Reject (duplicate, out of scope, user error)
support ticket status FB500 rejected \
--message "Duplicate of FB111. Please reply there if the issue recurs." --json
Always include --message — it's the user's only visibility into why.
Reassign to another team
support ticket assign FB123 \
--team "Butler Agent" \
--reason "Issue is with the Butler trading loop, not Axelrod swaps." --json
Rate-limited to 10/hour per token. After reassignment the ticket is no longer visible to this token.
Internal-only note (never emailed)
support ticket notes FB123 "User confirmed on-chain receipt; closing after final check." --json
notes REPLACES the field — read first if intent is to append.
Error Handling
All non-success exits include a machine-readable code in --json mode:
| Code | Meaning | What to do |
|---|
MISSING_TOKEN | ACP_AGENT_TOKEN not set | Export the token and retry |
UNAUTHORIZED | Token invalid / revoked | Rotate the token; do not retry |
FORBIDDEN | Super-admin token or no team | Use a team-scoped token |
NOT_FOUND | Ticket missing or not on your team | Skip the ticket |
CONFLICT | Terminal status OR version mismatch | Re-fetch + retry for versions; escalate for terminal |
KEY_REUSED | Idempotency-Key used on different endpoint | Fresh key, retry |
RATE_LIMITED | 30/min general, 5/min email, 10/hr reassign | Back off, retry after window |
SERVER_ERROR | 5xx | Exponential backoff |
NETWORK_ERROR | Connect timeout / DNS / TLS (no HTTP response) | Transient — retry with the same --idempotency-key (server may have committed) |
BAD_STATUS / BAD_INPUT | Local validation failed | Fix input, retry |
The CLI auto-reads version before every mutation, so version mismatches only happen when two writers race. On CONFLICT with a currentVersion recovery hint, retry once.
Retrying safely
By default, every mutation auto-generates a fresh Idempotency-Key per invocation. That means a retry after a NETWORK_ERROR (connect timeout, transport error — when the server may or may not have committed) will re-execute and can double-send emails or double-apply notes.
For retry safety, pass --idempotency-key <key> with a caller-stable value and reuse the same key across retries. The server caches the original response for 24h per (token, key) pair and replays it instead of re-executing.
KEY="resolve-FB123-$(date +%Y-%m-%d)"
support ticket status FB123 resolved --message "Fixed" --idempotency-key "$KEY" --json
support ticket status FB123 resolved --message "Fixed" --idempotency-key "$KEY" --json
Keys are scoped to (token, METHOD path). Reusing a key across different endpoints returns KEY_REUSED (422) — pick a fresh key per operation.
Guardrails
- Terminal is terminal. Once a ticket is
resolved, refunded, or rejected, the API will not transition it back. Do not loop retrying — surface to a human.
- Plain text only. Comments and messages render as escaped HTML in the email template. Never send markup.
- Emails are irreversible.
ticket status on an email-contact ticket and ticket comment --email both send real user mail. Confirm the body before calling.
- Do not reassign to clear a queue. Reassignment is for genuine misrouting; the rate limit is there because reassign-churn is a known anti-pattern.
Reference
references/api.md — full endpoint / request / response reference for the underlying API.