# Permacc

> ALWAYS use when URLs in a manuscript need permanent archived copies — 'perma these links', 'archive the URLs in my footnotes', 'add perma links', 'the journal wants perma.cc cites', 'make sure these links don't rot', 'archive this site before it changes', 'snapshot these URLs', 'perma.cc', or when cite-checking a draft with web sources. ALSO use when perma returns 'you've reached your usage limit' despite an institutional account, or when asking which perma folder an account may archive into.

- Skill: `edwinhu/permacc` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add edwinhu/permacc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/edwinhu/permacc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: edwinhu (https://skillmd.com/u/edwinhu)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/edwinhu/permacc

---


# perma.cc

Archives a URL to a permanent, court-citable snapshot. The whole skill exists
because **one HTTP 400 means three different things**, and the message names
only the least likely one.

```
HTTP 400  "You've reached your usage limit."
              │
              ├── folder sent as ?query= → ignored, billed to PERSONAL quota
              ├── no folder at all       → billed to PERSONAL quota
              └── genuinely unsponsored  → the only case the message describes
```

## CLI

```bash
PC="${CLAUDE_SKILL_DIR}/scripts/permacc.py"

uv run --script "$PC" status          # who the key is, and whether a sponsored folder exists
uv run --script "$PC" folders         # every folder, with the sponsored one marked
uv run --script "$PC" archive URL... --auto-folder
uv run --script "$PC" archive --from-json inventory.json --out archives.json --auto-folder
uv run --script "$PC" verify --out archives.json     # re-check captures on an existing record
uv run --script "$PC" delete --out archives.json --failed --private-if-undeletable
```

`--auto-folder` resolves the sponsored folder itself, which is the option to
reach for; `--folder <id>` pins one when an account has several. `--out` makes
the run resumable — already-archived URLs are skipped, so a rate limit or a
network drop costs nothing on the retry.

The key is read from `--api-key`, then `PERMACC_API_KEY`, then
`PERMACC_API_KEY_FILE` (the agenix convention — the environment carries a
*path* to the decrypted secret, not the secret).

## Facts

- **`folder` must be a JSON BODY field, not a query parameter.** `?folder=376861`
  is accepted, silently ignored, billed against the personal quota, and 400s
  once that quota is spent. Verified side by side on one URL against a
  sponsored account: query param → 400, body field → **201**. Nothing in the
  response distinguishes "ignored your folder" from "you are out of links",
  so this reads as a plan problem and sends people to the pricing page.

- **Sponsorship hangs off the FOLDER, not the organization.** `GET
  /v1/organizations/` returned `registrar: None` for an account that was
  already sponsored; the affiliation lives on a folder in `GET /v1/user/`
  carrying `registrar: 16` / `registrar_name: "University of Virginia School
  of Law Arthur J. Morris Law Library"`. Diagnosing a cap from the
  organizations endpoint concludes "not sponsored" about a sponsored account.

- **The folder list is `top_level_folders`, not `folders`.** A wrong key
  returns `[]`, which this skill's own first draft reported as "no sponsored
  folder — ask a registrar to add you." The failure mode of guessing a
  response key here is a confident false negative, not an error.

- **The auth scheme is `ApiKey`, not `Bearer`.** A Bearer header authenticates
  as anonymous and fails later with a permission error rather than a 401, so
  the traceback points at the wrong thing.

- **The free personal tier is 10 links/month.** A cite-check script with no
  folder therefore works for the first ten footnotes and dies on the
  eleventh — the shape of bug that looks like a flaky API.

- **HTTP 201 means a link was MINTED, not that the page was fetched.** The two
  come apart on any site that blocks crawlers. SSRN is behind Cloudflare, so
  every SSRN archive returns 201 and then captures the challenge page:
  `captures[role=primary].status == "failed"`, `title` set to `ssrn.com`
  instead of a paper title. Measured on one manuscript: **22 of 28 captured,
  and all 6 failures were SSRN.** A perma link resolving to an interstitial is
  worse than the live URL — it looks archived and is not, and nothing in the
  create response says so. `archive` now polls the capture (capture is async,
  so the status right after 201 is `pending` and a single check would call a
  good archive bad); `verify` re-checks an existing record.

- **Some sites cannot be perma'd at all, and that is the right answer.** For
  SSRN, cite the live URL: an `abstract_id` is a permanent identifier that
  SSRN does not recycle, so the rot perma exists to prevent barely applies.
  Law reviews routinely print SSRN URLs unarchived for working papers.

- **A perma link is deletable for 24 HOURS and permanent after that.** Past
  the window `DELETE` returns **403**, and the remedy is `is_private: true`,
  which unpublishes it without removing it. So a cleanup that assumes DELETE
  works half-succeeds on any set spanning more than a day — the normal shape
  of a manuscript's archive set. Observed: of 6 bad links, the 4 made that day
  deleted (204) and the 2 from six months earlier did not.

- **`private_reason` is a closed enum the API will not enumerate.** A wrong
  value 400s with a message that lists no alternatives, so it has to be
  guessed. `user` is the one meaning "I no longer want this public".

- **Archiving is not idempotent.** Two POSTs for one URL make two perma links.
  Pass `--out` and let the tool skip what it already has, rather than
  re-running a loop and quietly doubling a manuscript's archive set.

## Red flags — STOP

| Action | Why wrong | Do instead |
|---|---|---|
| About to report "your perma account is out of links" from a 400 | Three causes share that message; only one is a real cap | Run `folders`, then retry with `--auto-folder` |
| About to check sponsorship with `/v1/organizations/` | It reports `registrar: None` for sponsored accounts | Read `top_level_folders` from `/v1/user/` |
| About to pass the folder as `?folder=` | Silently ignored, billed to the personal quota | Send it in the JSON body (the script does) |
| About to loop `requests.post` over a URL list | No resume, no dedupe; a mid-run failure double-archives on retry | `archive --from-json … --out …` |
| About to tell a user to buy a plan | A law library registrar gives faculty unlimited links free | Have them added to the registrar's org first |
| About to record a 201 as "archived" | 201 mints a link; the capture can still fail, and SSRN always does | Let `archive` verify, or run `verify --out` |
| About to script `DELETE` over a set of links | Only links under 24h old delete; older ones 403 and need `is_private` | `delete --failed --private-if-undeletable` |
| About to paste the key into a script or `.env` | It is a long-lived credential | agenix (see below), read via `PERMACC_API_KEY_FILE` |

## Storing the key

```bash
~/nix/add-api-keys.sh permacc-api-key      # encrypts to ~/nix-secrets, prints the wiring
```

Then in `modules/shared/home-secrets.nix`: an `age.secrets` entry, a
`PERMACC_API_KEY_FILE` session variable, and a `get-permacc-api-key` alias.
`nix-secrets` is a flake input **pinned by revision**, so the secret must be
pushed *and* the lock bumped (`nix flake update nix-secrets`) before a rebuild
can see it.

## Getting a sponsored account

Perma registrars are institutions — mostly law libraries and courts. A faculty
member or student at one gets unlimited links at no cost by asking to be added
to the institution's perma.cc organization. That is the fix for a cap, and it
is usually a single email. Until it lands, `folders` prints `[personal ]` for
every row and every archive spends the 10/month allowance.

