API reference
Start with the runnable client example to construct real issuance and spend messages. The shortened base64 payloads below illustrate the envelope format and cannot be submitted as proofs.
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.