Axolotl is the MineSkin proxy used by SkinsRestorer. It forwards authenticated skin and cape requests without exposing the MineSkin API key, waits for queued skin jobs, and converts MineSkin URLs into encrypted skinsrestorer-axolotl:// URLs.
The service is a single Rust binary built with Axum and Tokio. It has no database or persistent local state.
- Rust 1.95.0
- A MineSkin API key
- A stable encryption secret
The repository includes rust-toolchain.toml, so Rustup selects the correct toolchain automatically.
Set the required environment variables:
export MINESKIN_API_KEY="your-mineskin-api-key"
export AES_SECRET_KEY="a-long-random-secret"Start the server:
cargo runAxolotl listens on port 3000 by default. Open Swagger UI or fetch the OpenAPI 3.1 document.
Axolotl also loads variables from a local .env file when one exists.
| Variable | Required | Default | Purpose |
|---|---|---|---|
MINESKIN_API_KEY |
For MineSkin routes | None | Authenticates outbound MineSkin requests. The Bearer prefix is optional. |
AES_SECRET_KEY |
For encrypted URL routes | None | Derives the AES-256 key used for existing Axolotl URLs. Keep this value stable across deployments. |
DISCORD_WEBHOOK |
No | Disabled | HTTPS Discord webhook that receives five-minute status reports. |
PORT |
No | 3000 |
TCP port for the HTTP server. Railpack and Railway provide this automatically. |
RUST_LOG |
No | axolotl=info,tower_http=info |
Controls structured log filtering. |
Missing route-specific credentials return a JSON configuration error. The health endpoint remains available without them.
Set DISCORD_WEBHOOK to enable an operational report every five minutes. Each report includes:
- Process uptime, RSS memory, and the 1, 5, and 15-minute system load averages.
- Request totals split by successful responses, redirects, client errors, and server errors.
- Error rates plus average and histogram-based median and 95th-percentile request latency.
- Request volume for uploads, jobs, cape operations, URL decryption, health checks, and other routes.
- MineSkin request totals, errors, rate limits, response bytes, and latency.
- Unique client IPs, one-request clients, average requests per IP, and the traffic share generated by the busiest IP and busiest five IPs.
- A traffic pattern label that calls out a dominant client, a few dominant clients, broadly distributed traffic, or a mixed distribution.
Client addresses come from the trusted CF-Connecting-IP header when it is present, then fall back to the direct peer address. Raw addresses stay in memory for aggregation and are never included in the Discord payload. Axolotl tracks up to 100,000 client addresses in an unsent reporting window. Requests above that limit still count toward request totals but appear as unattributed traffic in the client breakdown.
Metrics advance only after Discord accepts a report. If delivery fails, the next attempt includes the full unsent period instead of discarding counters. The reporting window is stored in memory and resets when the process restarts.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Report service health. |
POST |
/mineskin/skins |
Upload a skin and wait for its MineSkin job to complete. |
GET |
/mineskin/jobs/{jobId} |
Fetch and sanitize a MineSkin job. |
GET |
/mineskin/capes |
List supported capes. Results are cached for five minutes. |
GET |
/mineskin/cape-support |
Check the configured account's cape grant and list usable capes. |
GET |
/mineskin/decrypt-url |
Decrypt an Axolotl URL into its original https://minesk.in/ URL. |
Skin uploads accept up to 5 MiB. Axolotl processes up to 16 uploads at once and returns 503 when every upload slot is occupied. Upload bodies and complete upload workflows have deadlines, while MineSkin polling can run for at most five minutes and returns 504 if processing does not finish in time.
New encrypted URLs use an authenticated v2 payload. The decryption endpoint continues to accept URLs created by the legacy AES-CBC service.
Railpack detects the root Cargo.toml, selects Rust 1.95.0 from rust-toolchain.toml, builds the release binary, and starts ./bin/axolotl. No custom build or start command is required.
Configure MINESKIN_API_KEY and AES_SECRET_KEY in the deployment environment, then deploy the repository. On Railway, Axolotl listens on the injected PORT and exposes /health for deployment health checks.
Pushes to main build a release image with Railpack and publish it to
ghcr.io/skinsrestorer/axolotl. Each build publishes three tags:
latest: the latest published build frommainmain: the latest published build frommainsha-<12-character-commit-sha>: the build for a specific commit
The workflow uses the ubuntu-24.04 runner and authenticates
with GITHUB_TOKEN, with packages: write permission.
Put the required environment variables in a local .env file. Then run:
docker run -d --name axolotl --restart unless-stopped \
--env-file .env -p 3000:3000 ghcr.io/skinsrestorer/axolotl:latestThe service is available at http://localhost:3000.
Run the same checks used by CI:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo test --all-targets --all-features --locked
cargo build --release --lockedThe tests include a fixed compatibility vector from the previous service. This protects existing encrypted URLs while the implementation remains in Rust.