API reference
Base URL https://anonymous-credit-tokens.info. Two issuers share one API shape: {scheme} below is act (classical) or pq (post-quantum). Bodies are JSON; binary fields are base64 (standard alphabet, padded). The scheme-native bytes inside request and response fields are produced and consumed by the two Rust crates listed at the bottom; the JSON layer never interprets them beyond hashing.
GET /api/v1/{scheme}/params
Issuer parameters and public key. domain_separator appears for act so third-party clients can reconstruct the exact parameter derivation; pq carries application_context (currently anonymous-credit-tokens.info/pq/prod/e1) instead. The PQ public key is large, about 106 KB before base64. The status field states the deployment's experimental status machine-readably.
curl -s https://anonymous-credit-tokens.info/api/v1/act/params
{
"scheme": "act",
"epoch": "e1",
"public_key": "5A3vPqkQ...issuer public key, base64...",
"domain_separator": "ACT-v1:anonymous-credit-tokens.info:act:prod:e1",
"max_credits_per_issue": 1024,
"challenge_ttl_seconds": 900,
"status": "experimental and unaudited; credits have no monetary value and may be erased at any time"
}POST /api/v1/{scheme}/challenge
Mints a single-use proof-of-work challenge for an issuance of credits, clamped to 1 through 1024. Difficulty is 18 leading zero bits at 1 credit plus one bit per doubling (so 24 bits for 64 credits, 28 at the 1024 cap). The challenge expires 900 seconds after minting.
curl -s -X POST https://anonymous-credit-tokens.info/api/v1/act/challenge \
-H 'content-type: application/json' \
-d '{"credits": 64}'
{
"challenge_id": "m3S1kQyLQFqCePYtcdMylg==",
"salt": "9uV1J6kQxJ2gUuKfW0bqQw==",
"difficulty_bits": 24,
"credits": 64,
"expires_at": "2026-07-19T17:45:00Z"
}Mining
Build the scheme-native issuance request first, because the work is bound to its bytes. Then search for an 8-byte little-endian nonce satisfying:
digest = BLAKE3.derive_key("anonymous-credit-tokens.info digest v1")
over parts ["act-issue", request_bytes] # "pq-issue" for pq
where each part is (len as u64 LE) || bytes
hash = BLAKE3.derive_key("anonymous-credit-tokens.info pow v1")
over scheme_str || challenge_id || salt || digest || nonce_u64_le
accept when leading_zero_bits(hash) >= difficulty_bitsBoth BLAKE3 invocations use derive-key mode with the quoted strings as the context. Work is bound to the challenge and to the exact request, so it cannot be mined ahead, reused, or applied to a different issuance.
POST /api/v1/{scheme}/issue
Exchanges a solved challenge and an issuance request for the issuance response. nonce is the 8-byte little-endian solution, base64. Consuming the challenge and storing the response happen in one atomic step: resending the identical envelope replays the stored response with replayed: true, and a different request against a consumed challenge fails with the final challenge_mismatch.
curl -s -X POST https://anonymous-credit-tokens.info/api/v1/act/issue \
-H 'content-type: application/json' \
-d '{
"challenge_id": "m3S1kQyLQFqCePYtcdMylg==",
"nonce": "vAkAAAAAAAA=",
"request": "hkNZ2m4vTq...issuance request, base64..."
}'
{
"response": "qL8wXf3d...issuance response, base64...",
"credits": 64,
"replayed": false
}POST /api/v1/{scheme}/spend
Verifies a spend proof, records the nullifier, and returns the response that finishes the client's change token. requested_return (default 0) asks the issuer to return part of the spent amount; it is granted up to the spend amount, and it is part of the request's identity, so a retry must repeat it exactly. On act, spend proofs must carry a zero top-up; this issuer has no policy that authorizes top-ups.
curl -s -X POST https://anonymous-credit-tokens.info/api/v1/act/spend \
-H 'content-type: application/json' \
-d '{
"request": "9tKqYc1R...spend proof, base64...",
"requested_return": 5
}'
{
"response": "e2NwUvHa...refund, base64...",
"returned": 5,
"replayed": false
}PQ spends additionally require settlement (which response family the request was built for) and input_kind (what kind of token is being spent), each direct or deferred. With settlement: "direct" the protocol fixes everything at proof time and returned is 0; with settlement: "deferred" the issuer chooses the return after verification, up to requested_return.
curl -s -X POST https://anonymous-credit-tokens.info/api/v1/pq/spend \
-H 'content-type: application/json' \
-d '{
"request": "A4tR7oQz...spend request, base64...",
"requested_return": 2,
"settlement": "deferred",
"input_kind": "direct"
}'
{
"response": "S1jVbe0K...spend response, base64...",
"returned": 2,
"replayed": false
}Retry semantics are the heart of this endpoint: an identical resend replays the stored response byte for byte with replayed: true; any different request on the same nullifier is a final double_spend. The full contract is on the failure semantics page.
GET /api/v1/{scheme}/nullifier/{hex}
Whether a nullifier has been consumed. {hex} is 64 hex characters (32 bytes). Anyone can query any nullifier; the value reveals nothing but spent-or-not.
curl -s https://anonymous-credit-tokens.info/api/v1/act/nullifier/6f2a9c81d44e03b7595d1c0e8a67f3b2410cd98e7b5a6d21f0c3841e9b72a5dd
{"spent": false}GET /api/v1/stats
Aggregate counters: nullifiers recorded per scheme and challenges consumed. Also rendered at /stats.
curl -s https://anonymous-credit-tokens.info/api/v1/stats
{"act_nullifiers": 1231, "pq_nullifiers": 208, "challenges_consumed": 1566}Errors
Every non-2xx response from a handler carries a JSON body with a stable machine-readable code and a human message:
{"code": "double_spend", "message": "nullifier already consumed by a different request"}| Code | HTTP | Meaning | Client action |
|---|---|---|---|
bad_request | 400 | Malformed envelope or base64, or a request rejected by policy (such as a classical top-up). | Final; fix the request. |
proof_invalid | 400 | The proof failed verification. No state was consumed. | Final; the spent token is still live. |
pow_invalid | 400 | The nonce does not satisfy the challenge for this request. | Keep mining, retry with a valid nonce. |
challenge_gone | 410 | Unknown or expired challenge. | Mint a new challenge. |
challenge_mismatch | 409 | Challenge already consumed by a different request. | Final. |
double_spend | 409 | Nullifier already consumed by a different request. | Final; retrying can never succeed. |
rate_limited | 429 | Too many requests. | Honor retry-after, slow down, retry. |
too_large | 413 | Body exceeds the route's cap. | Final for that request. |
internal | 500 | Server fault. | Retry later; identical retries stay safe. |
The 429 and 413 rejections come from middleware in front of the handlers and may carry a plain-text body instead of the JSON envelope; treat the HTTP status as authoritative for those two.
The replayed flag and retries
Issue and spend replies carry a replayed flag. true means this exact request was answered before and the stored response was returned unchanged, which is the normal recovery path after a dropped connection. Clients should persist their exact request bytes before sending and resend them unchanged on failure; rebuilding a randomized proof produces a different request and a final error.
Limits
Request bodies are capped at 16 KB on act routes and 256 KB on pq routes (PQ spend requests are large; see costs). All API routes are rate limited per client; expect 429 with a retry-after header under bursts. Handlers time out at 30 seconds.
MCP endpoint
A read-only MCP server for agents runs at https://anonymous-credit-tokens.info/mcp with three tools: get_params (scheme parameters and public key), check_nullifier (spent-or-not for a hex nullifier), and get_stats. Issuing and spending are deliberately not exposed as tools: they require mining and client-side secrets, which belong in a wallet.
Native byte formats
The request and response payloads are the schemes' own canonical encodings:
- anonymous-credit-tokens defines the classical issuance request/response, spend proof, and refund, in a TLS-presentation-language wire format.
- vole-act defines the PQ encodings, each carrying explicit version, parameter-set, input-kind, and settlement tags.