Skip to content
Back to skills

Client Lifecycle

ASecurity

Run the day-to-day client operations on a 3x-ui panel — onboarding with quotas and expiry, handing over links, bulk renewals, usage reports, and cleanup of depleted or orphaned accounts. Use when asked to create or provision users, extend or top up subscriptions, report on traffic or who is online, or tidy up a panel that has accumulated dead accounts.

  • 5 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
ai-agentsgonodegit

Works with

  • cli

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add pyworkload/3x-ui-mcp --skill client-lifecycle --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Client Lifecycle?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Client Lifecycle
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pyworkload-client-lifecycle/badge)](https://www.skillsdirectory.com/skills/pyworkload-client-lifecycle)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: client-lifecycle
description: Run the day-to-day client operations on a 3x-ui panel — onboarding with quotas and expiry, handing over links, bulk renewals, usage reports, and cleanup of depleted or orphaned accounts. Use when asked to create or provision users, extend or top up subscriptions, report on traffic or who is online, or tidy up a panel that has accumulated dead accounts.
---

# Client lifecycle

Clients in 3x-ui are email-keyed entities, not properties of an inbound: one client
can be attached to several inbounds and moved between them without being recreated.
`email` is just a unique label — it need not be an address.

## Units, which cause most of the mistakes

| Field | Unit |
|---|---|
| `expiry_time` (tool parameter) | Unix **milliseconds**. 0 = never |
| `expiryTime` (stored) | Same. A **negative** value is a duration, not a date — "start after first use" |
| `total_gb` (tool parameter) | **GB**, converted for you |
| `totalGB` (stored) | **Bytes**, despite the name |
| `limit_ip` | Simultaneous source IPs. 0 = unlimited |
| `reset` | Traffic reset cycle in days. 0 = no cycle |

A seconds-precision timestamp passed as `expiry_time` lands in 1970 and the client
is born expired. When someone asks for "30 days", compute from now in milliseconds.

Negative expiry is the right tool for prepaid trials. Store the duration as a
negative number of milliseconds and the panel converts it to an absolute deadline
of `now + duration` on the client's first traffic tick — so a 7-day trial handed
out in March still gives seven days to someone who first connects in June. The
conversion is computed once per email, so a client attached to several inbounds
lands on one consistent deadline.

## Onboarding

```
add_client inbound_ids=[7] email="alice" total_gb=100 expiry_time=<now+30d in ms> limit_ip=3
```

The UUID is generated by the panel when omitted — do not invent one. For
Trojan/Shadowsocks the panel generates the key server-side the same way.

`sub_id` groups clients under one subscription URL: clients sharing a `sub_id`
share a subscription. Give each user their own unless you deliberately want a
shared link. `group` is a free label the client-group tools operate on later, and
is worth setting at creation — retrofitting groups across hundreds of clients is
the tedious version of the same job.

For many at once, `bulk_create_clients` creates across the same inbounds, and
`bulk_attach_clients` attaches existing clients to more inbounds afterwards.

## Handing over access

```
get_subscription_links sub_id="<the client's subId>"
```

A subscription URL is better than a raw link: the user's app refreshes it, so
adding an inbound or changing a host group reaches them without a new QR code.
`get_all_inbound_links` is the raw list when a one-off link is genuinely wanted —
it summarises and links to the full set unless `full=true`.

Both carry credentials. Treat them as secrets: they are the account.

## Renewals and adjustments

```
bulk_adjust_clients emails=["alice","bob"] add_days=30 add_gb=50
```

Deltas, not absolutes, and both may be negative. Passing neither is an error
("no adjustment specified") rather than a no-op.

Clients are skipped rather than forced, each with a reason, and the reasons are
worth knowing because "renew everyone" can quietly renew almost nobody:

| Skip reason | Who it hits |
|---|---|
| `unlimited expiry` | `expiryTime` is 0 — adding days to "never" is meaningless |
| `unlimited traffic` | `totalGB` is 0 — same logic for quota |
| `reduction exceeds remaining time` | A negative `add_days` that would push the deadline into the past |
| `reduction exceeds delay window` | A negative `add_days` larger than an unstarted trial's whole duration |

Read that skipped list back to whoever asked instead of reporting a clean success.

Unstarted trials behave the way you would hope: adding days to a client with a
negative (not-yet-started) expiry lengthens the **trial duration** and leaves it
unstarted, rather than converting it to a fixed date. A 7-day trial topped up by 7
becomes a 14-day trial that still starts on first use.

For a single client, `update_client` overlays only the fields passed and preserves
everything else, the UUID above all. Never rebuild a client to change one field:
`delete_client` + `add_client` issues a new UUID and breaks the user's app.

Automatic renewal is a schedule on the client, not a job you run: `reset`
(every N days), `reset_day` (a day of the month) or, from panel v3.9.0,
`reset_weekday` (1-7, Mon-Sun), capped by `reset_max`. Weekly cannot be combined
with the other two; `update_client` clears the mode being left. Before promising
anyone a date, ask the panel — it computes in its own time zone:

```
preview_client_renewal email="alice" reset_weekday=1
```

`bulk_enable_clients` / `bulk_disable_clients` suspend without deleting — the right
move for non-payment, since re-enabling restores the same credentials.

## Reporting

```
list_clients_paged   # server-side filter, sort and page
get_client_traffic email="alice"
get_online_clients
get_last_online
```

`list_clients_paged` does the filtering on the panel; use it rather than pulling
every client and filtering locally, which on a large panel is slow and floods the
context. `get_last_online` is the fastest way to find accounts that are paid for
but unused, and `get_online_clients` the fastest way to find one credential shared
across many devices — cross-check with `get_client_ips` and `list_client_devices`.

On a multi-node panel, `get_online_clients_by_node` and `get_client_ips_by_node`
attribute the same data to the node that actually served it.

## Cleanup

Both of these are **destructive and panel-wide**:

```
delete_depleted_clients   # exhausted quota or past expiry
delete_orphan_clients     # attached to no inbound at all
```

"Depleted" has an exact definition, and it is narrower than it sounds — the panel
deletes a client only when

    reset = 0 AND ((total > 0 AND up + down >= total) OR (0 < expiryTime <= now))

Three groups are therefore safe by construction: clients on a **reset cycle**
(`reset` non-zero) are never touched, unlimited clients (`total` 0, `expiryTime` 0)
cannot match, and a trial that has not started yet (negative `expiryTime`) is not
expired. That makes the call safer than its name suggests — but it also means
"clean up the dead accounts" will quietly leave every cyclical client behind, which
is usually a surprise to whoever asked.

Enumerate before deleting. `list_clients_paged` with the matching filter shows what
is about to go; present that list, then delete. Orphans usually appear after an
inbound was removed without its clients, so a run of `delete_orphan_clients` right
after someone deleted an inbound is expected — one at a random moment is worth
questioning.

Lighter cleanups: `clear_client_ips` after a user legitimately changes network,
`clear_client_devices` or `delete_client_device` when HWID slots are full and a new
device is being refused.

## Migration and backup

`export_clients` produces the `{client, inboundIds}` array that `import_clients`
consumes, so it round-trips. It carries every UUID and password — keep it behind
the resource link rather than inlining it, and handle it as a credential file.
`import_clients` reports existing emails in a `skipped` list instead of
overwriting, so an import cannot clobber a live client.

Take an export (or `backup_to_telegram`) before any bulk operation. There is no
undo for the destructive ones.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.