Self-hosted ARC-56 program hash + ABI signature registry (unprivileged nginx, port 8080).
10K+
Look up a deployed Algorand app's compiled TEAL by SHA-256 hash and get back a real ARC-56 spec you can use to decode its calls - or look up a raw 4-byte ARC-4 method selector and get back the human-readable method signature it came from. This image is a self-hosted webserver: run it, and every lookup below is a plain HTTP request against your own container - no dependency on a third-party host at call time.
This is the same registry published at
scholtz.github.io/ARC56Registry and
maintained in scholtz/ARC56Registry,
repackaged as scholtz2/arc56-registry: an unprivileged
nginx container serving the
registry's static files, so a wallet can run its own local/private mirror instead of
depending on GitHub Pages being reachable at call time.
A wallet about to send an application call usually only has the app ID. It can fetch the app's compiled approval and clear programs from the network, but without a spec it has no idea what methods exist, what arguments they take, or how to render the call to a user instead of a wall of opaque bytes.
This registry closes that gap: every ARC-56 spec indexed by
scholtz/ARC56Registry has its compiled
approval and clear programs hashed with SHA-256, and each hash maps to a full copy of
a matching spec. Running this image gives you your own HTTP endpoint serving that
whole dataset, refreshed by re-pulling latest (or a dated tag) and restarting the
container, instead of hitting GitHub Pages for every lookup.
docker run -d --name arc56-registry -p 8080:8080 scholtz2/arc56-registry:latest
curl http://localhost:8080/README.md
Listens on port 8080 as an unprivileged, non-root nginx process (runs as uid
101, the image's default user) - no --user root, no --cap-add, no privileged
port binding needed to run it anywhere, including under container platforms that
enforce non-root policies. Visiting http://localhost:8080/ in a browser serves a
landing page with the same content as this README, plus worked examples.
docker pull scholtz2/arc56-registry:latest
# or pin to a specific day's snapshot, e.g.:
docker pull scholtz2/arc56-registry:2026-07-19
latest always points at the most recently published snapshot; a YYYY-MM-DD-dated
tag (UTC) is kept for every past publish so you can pin to a known snapshot instead of
floating on latest.
/usr/share/nginx/html/approval-programs/<hash[:3]>/<hash>.txt # keyed by sha256(byteCode.approval)
/usr/share/nginx/html/approval-programs/<hash[:3]>/<hash>.arc56.json # keyed by sha256(byteCode.approval)
/usr/share/nginx/html/clear-programs/<hash[:3]>/<hash>.txt # keyed by sha256(byteCode.clear)
/usr/share/nginx/html/clear-programs/<hash[:3]>/<hash>.arc56.json # keyed by sha256(byteCode.clear)
/usr/share/nginx/html/abi-signatures/<selector[:2]>/<selector>.txt # keyed by the method's ARC-4 selector
/usr/share/nginx/html/abi-signatures/<selector[:2]>/<selector>.json # keyed by the method's ARC-4 selector
/usr/share/nginx/html/arc56.links.csv # the full registry: every discovered ARC-56 spec URL
/usr/share/nginx/html/README.md # this file
/usr/share/nginx/html/index.html # the landing page served at /
nginx serves all of the above at the container's document root, so from outside the
container every path above is just http://<host>:8080/approval-programs/...,
/clear-programs/..., /abi-signatures/..., /arc56.links.csv, /README.md, or
/.
Splitting on the hash's/selector's first few hex characters keeps any one folder from
holding thousands of files. Each program-hash .txt file contains exactly one line: a
raw.githubusercontent.com URL pinned to the commit that last touched the matching
ARC-56 spec, so it keeps resolving to the exact spec that produced this hash even
after the source file is edited or replaced later. The .arc56.json file right next
to it is a byte-for-byte copy of that same spec, so you can skip resolving the URL
entirely and read the full spec straight from this server. Each abi-signatures/
.txt file contains the plain-text ABI method signature itself, e.g.
add(uint64,uint64)uint128, and its .json file holds that signature plus the sorted
list of approval-program hashes of every indexed app known to expose it.
GET /v2/applications/{app-id},
and base64-decode params.approval-program and/or params.clear-state-program.sha256(program_bytes), hex-encoded, lowercase.http://<host>:8080/approval-programs/<hash[:3]>/<hash>.arc56.json or
.../clear-programs/<hash[:3]>/<hash>.arc56.json. A 404 just means this registry
hasn't indexed a spec producing that hash yet, not an error..txt file at the same path
instead holds a durable, commit-pinned URL to the spec's source location, if you
want that instead of the copy.)Python
import base64, hashlib, json, urllib.request
app_id = 123456789
info = json.load(urllib.request.urlopen(f"https://mainnet-api.4160.nodely.dev/v2/applications/{app_id}"))
approval = base64.b64decode(info["params"]["approval-program"])
digest = hashlib.sha256(approval).hexdigest()
lookup_url = f"http://localhost:8080/approval-programs/{digest[:3]}/{digest}.arc56.json"
try:
spec = json.load(urllib.request.urlopen(lookup_url))
print("Found spec for", spec["name"])
except urllib.error.HTTPError:
print("No ARC-56 spec indexed for this program yet")
TypeScript (Node.js 18+, built-in fetch)
import { createHash } from "node:crypto";
const appId = 123456789;
const info = await (await fetch(`https://mainnet-api.4160.nodely.dev/v2/applications/${appId}`)).json();
const approval = Buffer.from(info.params["approval-program"], "base64");
const digest = createHash("sha256").update(approval).digest("hex");
const lookupUrl = `http://localhost:8080/approval-programs/${digest.slice(0, 3)}/${digest}.arc56.json`;
const res = await fetch(lookupUrl);
if (res.ok) {
const spec = await res.json();
console.log("Found spec for", spec.name);
} else {
console.log("No ARC-56 spec indexed for this program yet");
}
C# (.NET 8+)
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
using var http = new HttpClient();
var appId = 123456789;
var info = await http.GetFromJsonAsync<JsonElement>($"https://mainnet-api.4160.nodely.dev/v2/applications/{appId}");
var approval = Convert.FromBase64String(info.GetProperty("params").GetProperty("approval-program").GetString()!);
var digest = Convert.ToHexStringLower(SHA256.HashData(approval));
var lookupUrl = $"http://localhost:8080/approval-programs/{digest[..3]}/{digest}.arc56.json";
var response = await http.GetAsync(lookupUrl);
if (response.IsSuccessStatusCode)
{
var spec = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"Found spec for {spec.GetProperty("name").GetString()}");
}
else
{
Console.WriteLine("No ARC-56 spec indexed for this program yet");
}
Every ARC-4/ARC-56 application call's first argument is a 4-byte method selector:
the first 4 bytes of SHA-512/256 (not SHA-256) over the method's ABI signature
string, name(argtype,argtype,...)returntype. If you've extracted that selector from
a transaction but don't have the app's ARC-56 spec, this registry can still resolve it
to a human-readable signature:
8aa3b61f.http://<host>:8080/abi-signatures/<selector[:2]>/<selector>.json from your
running container. A 404 just means this registry hasn't indexed a method with that
selector yet, not an error.{"abi": "<signature>", "apps": [...]} - abi is the
plain-text ABI signature, e.g. add(uint64,uint64)uint128, and apps is the
sorted list of approval-program hashes of every indexed app known to expose it.Python
import urllib.request, json
selector = "8aa3b61f" # first 4 bytes of an app call's method-call arg, hex-encoded
lookup_url = f"http://localhost:8080/abi-signatures/{selector[:2]}/{selector}.json"
try:
entry = json.load(urllib.request.urlopen(lookup_url))
print("Selector resolves to", entry["abi"]) # add(uint64,uint64)uint128
print("Known apps:", entry["apps"])
except urllib.error.HTTPError:
print("No ABI method indexed for this selector yet")
TypeScript (Node.js 18+, built-in fetch)
const selector = "8aa3b61f"; // first 4 bytes of an app call's method-call arg, hex-encoded
const lookupUrl = `http://localhost:8080/abi-signatures/${selector.slice(0, 2)}/${selector}.json`;
const res = await fetch(lookupUrl);
if (res.ok) {
const entry = await res.json();
console.log("Selector resolves to", entry.abi); // add(uint64,uint64)uint128
console.log("Known apps:", entry.apps);
} else {
console.log("No ABI method indexed for this selector yet");
}
C# (.NET 8+)
using System.Linq;
using System.Net.Http.Json;
using System.Text.Json;
using var http = new HttpClient();
var selector = "8aa3b61f"; // first 4 bytes of an app call's method-call arg, hex-encoded
var lookupUrl = $"http://localhost:8080/abi-signatures/{selector[..2]}/{selector}.json";
var response = await http.GetAsync(lookupUrl);
if (response.IsSuccessStatusCode)
{
var entry = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"Selector resolves to {entry.GetProperty("abi").GetString()}"); // add(uint64,uint64)uint128
var apps = entry.GetProperty("apps").EnumerateArray().Select(a => a.GetString());
Console.WriteLine($"Known apps: {string.Join(", ", apps)}");
}
else
{
Console.WriteLine("No ABI method indexed for this selector yet");
}
The three lookups above only cover ARC-56 specs whose compiled programs have been
successfully hashed. arc56.links.csv is the underlying source list this whole
project is built from: every *.arc56.json file discovered on GitHub, one row per
spec, with ActiveFrom/ActiveUntil columns marking whether it's currently
considered active (rows are never deleted - a retired spec is deactivated by setting
ActiveUntil, not removed). Useful if you want to enumerate every known spec
directly, independent of the hash tables:
curl -s http://localhost:8080/arc56.links.csv | head
ARC56URL,ActiveFrom,ActiveUntil
https://raw.githubusercontent.com/<owner>/<repo>/HEAD/path/to/spec.arc56.json,2026-07-16,
A row is active when ActiveFrom <= today and (ActiveUntil is empty or in the
future). See
docs/arc56-links-pipeline.md
for the full column/lifecycle rules. Note this file is not part of the GitHub
Pages site - it's only published here and in the source repo itself.
If you'd rather have the folders as plain files (e.g. to bundle into another image's
build) than query them over HTTP, docker cp still works - this is a normal
filesystem, not a scratch/data-only image:
id=$(docker create scholtz2/arc56-registry:latest)
docker cp "$id":/usr/share/nginx/html/approval-programs ./approval-programs
docker cp "$id":/usr/share/nginx/html/clear-programs ./clear-programs
docker cp "$id":/usr/share/nginx/html/abi-signatures ./abi-signatures
docker cp "$id":/usr/share/nginx/html/arc56.links.csv ./arc56.links.csv
docker rm "$id"
Or in a multi-stage Dockerfile:
FROM scholtz2/arc56-registry:latest AS registry
FROM your-wallet-base-image
COPY --from=registry /usr/share/nginx/html/approval-programs /data/approval-programs
COPY --from=registry /usr/share/nginx/html/clear-programs /data/clear-programs
COPY --from=registry /usr/share/nginx/html/abi-signatures /data/abi-signatures
COPY --from=registry /usr/share/nginx/html/arc56.links.csv /data/arc56.links.csv
Both program-hash tables and the ABI signature registry are regenerated daily by
generate-hash-registry.yml,
and arc56.links.csv is separately updated daily by
update-arc56-links.yml.
This image is rebuilt and republished after either one finishes, by
publish-docker-hash-registry.yml
arc56.links.csv, which the Pages site doesn't publish),
just served from your own container instead. Re-pull latest (or a newer dated tag)
and restart the container to pick up the newest snapshot. If two indexed specs
compile to the same program bytes, the hash points at whichever spec file is larger
(generally the more complete one) - see
docs/hash-registry.md
for the exact tie-breaking rule and the ABI signature registry's own rules.Content type
Image
Digest
sha256:76129bee5…
Size
28.6 MB
Last updated
about 3 hours ago
docker pull scholtz2/arc56-registry