Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when a val's HTTP endpoints should not be open to the whole internet — limiting an app to a team, understanding why an endpoint redirects to a login page, letting a webhook through, or identifying which Val Town user is viewing an app. Covers app access (`httpPrivacy`), org grants, bypass tokens for automation, and the `X-Val-Town-User` identity header. For building your own login flow inside a val, see the `oauth` skill instead.
.claude/skills/hashgraph-online-restricted-access/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -17% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 12% | 0% |
| case-04 | ✗→✓ | ▲ Improved | -27% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 69% | 0% |
A val has two independent access settings. Changing one does not change the other:
privacy: public / unlisted / private) — who can read the source on val.town.httpPrivacy: public / restricted) — who can call the val's HTTP endpoints.A val can have private code and a wide-open endpoint, or public code and a locked-down endpoint. update_val's privacy field only moves the first one; app access is changed with set_http_privacy.
Restricted app access is available to organizations that have the feature enabled. Vals created in such an org may default to restricted — always read httpPrivacy off a get_val_detail, list_vals, create_val, or remix_val response rather than assuming a new val's URL is open.
Two different things both sound like "make my app require a login":
std/oauth (see the oauth skill) runs inside your val: you wrap your handler, and anyone with a Val Town account can log in. You control the session and can build per-user features.Pick restricted access for an internal tool that only your team should reach. Pick std/oauth when any Val Town user may sign in and the app needs its own notion of a logged-in user.
Don't stack them by accident. Adding oauthMiddleware to an already-restricted val means the visitor authenticates twice — once at the gate, once in your code. If a restricted val needs to know who is viewing, use the identity header below instead of adding OAuth.
Access is granted to organizations, not individual people. A viewer gets in when the val has a grant to an org and that viewer is a member of it. Removing either one revokes access on the very next request — nothing is cached for the length of a session.
Grants come from:
add_allowed_user grants an org, list_allowed_users shows current grants, remove_allowed_user revokes one.list_allowed_users alongside direct grants.An unauthenticated request does not reach the val. The platform answers with a 302 redirect to a Val Town login or authorization page. This is the single most common source of confusion when debugging a restricted val:
fetch_val_endpoint reports a redirect it won't follow.curl shows a 302 to val.town instead of your response.403 explaining they need access to their organization.None of these mean the val's code is broken. Check httpPrivacy first — if it's restricted, the gate is doing its job. Make the val public with set_http_privacy, grant the caller's org, or use a bypass token.
Machines can't complete a login redirect, so a restricted val that receives webhooks (Stripe, GitHub, a cron job in another val) needs a bypass token — a secret scoped to that one val.
Create it with create_bypass_token; the secret is shown once and cannot be retrieved again. Manage tokens with list_bypass_tokens and revoke_bypass_token.
Present it either way:
ts// Header (preferred — keeps the secret out of logs and referrers) await fetch(url, { headers: { "X-Val-Town-Access": Deno.env.get("MY_BYPASS_TOKEN")! } }); // Query param (for services that only accept a URL, e.g. some webhook configs) await fetch(`${url}?val_town_access=${Deno.env.get("MY_BYPASS_TOKEN")}`);
The platform strips the header and the query param before your handler runs, so your code never sees them. A bypass-token request carries no viewer identity — it is an anonymous machine caller.
For a human viewer who came in through the gate, the platform forwards a short-lived signed X-Val-Town-User header. It is not the identity itself — exchange it for the viewer's profile using the val's own API token, which Val Town injects as the valtown environment variable:
tsconst IDENTITY_HEADER = "X-Val-Town-User"; /** Returns the viewer's public profile, or null when there isn't one. */ async function getViewer(req: Request) { const signed = req.headers.get(IDENTITY_HEADER); if (!signed) return null; const res = await fetch("https://api.val.town/v3/val/viewer", { headers: { Authorization: `Bearer ${Deno.env.get("valtown")}`, [IDENTITY_HEADER]: signed, }, }); if (!res.ok) return null; // { id, username, type, bio, profileImageUrl, url, links } return await res.json(); }
Rules that matter:
!-assert it or index into a null result.The transport above (X-Val-Town-User plus the /v3/val/viewer exchange) is how this works today and may change; the three rules hold regardless.
| Task | Tool | | --- | --- | | Check the current setting | get_val_detail (httpPrivacy field) | | Make an endpoint public or restricted | set_http_privacy | | See who has access | list_allowed_users | | Grant / revoke an org | add_allowed_user / remove_allowed_user | | Create / list / revoke automation secrets | create_bypass_token / list_bypass_tokens / revoke_bypass_token |
Restricted vals can only be iframed by val.town, so an embed of one on an external site will be blocked by the browser regardless of who's logged in.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 27,724 | 13,984 | -50% | 1 | 1 | 0% | 3,804 | 3,151 | -17% | 0 | 0 | — |
case-02 | fail→pass | 16,565 | 12,676 | -23% | 1 | 1 | 0% | 3,222 | 3,989 | +24% | 0 | 0 | — |
case-03 | fail→pass | 21,985 | 13,226 | -40% | 1 | 1 | 0% | 2,911 | 3,265 | +12% | 0 | 0 | — |
case-04 | fail→pass | 24,695 | 5,535 | -78% | 1 | 1 | 0% | 3,581 | 2,601 | -27% | 0 | 0 | — |
case-05 | pass→pass | 9,554 | 14,071 | +47% | 1 | 1 | 0% | 1,709 | 3,121 | +83% | 0 | 0 | — |
case-06 | pass→pass | 9,456 | 10,914 | +15% | 1 | 1 | 0% | 1,307 | 2,782 | +113% | 0 | 0 | — |
case-07 | fail→pass | 10,863 | 6,265 | -42% | 1 | 1 | 0% | 1,610 | 2,714 | +69% | 0 | 0 | — |
case-08 | fail→pass | 20,413 | 9,204 | -55% | 1 | 1 | 0% | 2,495 | 3,337 | +34% | 0 | 0 | — |
case-09 | fail→pass | 11,964 | 10,337 | -14% | 1 | 1 | 0% | 1,803 | 2,454 | +36% | 0 | 0 | — |
case-10 | pass→pass | 35,819 | 9,620 | -73% | 1 | 1 | 0% | 1,822 | 2,575 | +41% | 0 | 0 | — |
case-11 | fail→pass | 29,004 | 7,539 | -74% | 1 | 1 | 0% | 2,528 | 2,106 | -17% | 0 | 0 | — |
case-12 | pass→pass | 16,886 | 11,169 | -34% | 1 | 1 | 0% | 2,800 | 2,881 | +3% | 0 | 0 | — |
case-13 | pass→pass | 11,830 | 5,862 | -50% | 1 | 1 | 0% | 2,000 | 2,541 | +27% | 0 | 0 | — |
case-14 | pass→pass | 24,584 | 10,926 | -56% | 1 | 1 | 0% | 2,024 | 2,540 | +25% | 0 | 0 | — |
case-15 | pass→pass | 18,605 | 6,695 | -64% | 1 | 1 | 0% | 1,402 | 2,768 | +97% | 0 | 0 | — |
case-16 | fail→pass | 21,317 | 2,871 | -87% | 1 | 1 | 0% | 1,841 | 2,112 | +15% | 0 | 0 | — |
case-17 | fail→pass | 20,434 | 9,594 | -53% | 1 | 1 | 0% | 1,640 | 2,412 | +47% | 0 | 0 | — |
case-18 | fail→pass | 34,654 | 7,586 | -78% | 1 | 1 | 0% | 2,476 | 1,923 | -22% | 0 | 0 | — |
case-19 | fail→pass | 38,665 | 45,511 | +18% | 1 | 1 | 0% | 1,218 | 2,149 | +76% | 0 | 0 | — |
case-20 | fail→pass | 81,300 | 53,300 | -34% | 1 | 1 | 0% | 876 | 2,214 | +153% | 0 | 0 | — |
case-21 | fail→pass | 49,830 | 150,959 | +203% | 1 | 1 | 0% | 3,749 | 2,836 | -24% | 0 | 0 | — |
case-22 | pass→pass | 94,053 | 10,750 | -89% | 1 | 1 | 0% | 693 | 2,062 | +198% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.