- OpenAPI spec:
docs/openapi.yaml - Postman collection:
docs/credence.postman_collection.json
| Environment | URL |
|---|---|
| Local development | http://localhost:3000 |
| Production | (configured via BASE_URL env var) |
All endpoints are publicly readable. Supply an X-API-Key header to unlock
the premium rate tier.
X-API-Key: <your-key>
| Header present | Tier |
|---|---|
| No | standard |
| Yes (valid key) | premium |
| Yes (invalid key) | standard |
All :address path parameters must be Ethereum addresses:
0x followed by exactly 40 hexadecimal characters.
Accepted in any case (EIP-55 checksummed or all lower-case).
Valid: 0xf39Fd6e51aad88F6f4ce6aB8827279cffFb92266
Invalid: 0x1234, f39fd6... (no 0x prefix), not-an-address
Returns service liveness.
GET /api/health
Response 200
{
"status": "ok",
"service": "credence-backend"
}Returns the computed trust score and identity data for an Ethereum address.
GET /api/trust/:address
Score algorithm
The score is an integer [0, 100] built from three components:
| Component | Max pts | Maxes when |
|---|---|---|
| Bond amount | 50 | ≥ 1 ETH bonded |
| Bond duration | 20 | bonded ≥ 365 days |
| Attestations | 30 | ≥ 5 attestations |
Path parameters
| Parameter | Type | Description |
|---|---|---|
address |
string | Ethereum address — 0x-prefixed, 40 hex chars (EIP-55 or lower-case) |
Headers (optional)
| Header | Description |
|---|---|
X-API-Key |
API key for premium rate tier |
Responses
| Status | Condition |
|---|---|
200 |
Identity found; returns TrustScore object |
400 |
Address format invalid |
404 |
No identity record for this address |
200 example — fully bonded identity
{
"address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"score": 100,
"bondedAmount": "1000000000000000000",
"bondStart": "2024-01-15T00:00:00.000Z",
"attestationCount": 5,
"agreedFields": {
"name": "Alice",
"role": "validator"
}
}200 example — unbonded identity (score 0)
{
"address": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc",
"score": 0,
"bondedAmount": "0",
"bondStart": null,
"attestationCount": 0
}400 example
{
"error": "Invalid address format. Expected an Ethereum address: 0x followed by 40 hex characters."
}404 example
{
"error": "No identity record found for address 0x1234567890123456789012345678901234567890."
}Response fields
| Field | Type | Description |
|---|---|---|
address |
string | Normalised (lower-case) Ethereum address |
score |
integer 0–100 | Computed trust score |
bondedAmount |
string (bigint wei) | Amount bonded in wei |
bondStart |
string | null | ISO 8601 timestamp when the bond was first posted |
attestationCount |
integer | Number of on-chain attestations |
agreedFields |
object? | Key/value pairs the identity has explicitly attested to (omitted if none) |
cURL examples
# Standard tier
curl http://localhost:3000/api/trust/0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
# Premium tier
curl -H "X-API-Key: my-key" \
http://localhost:3000/api/trust/0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
# Not found (valid address, no record)
curl http://localhost:3000/api/trust/0x1234567890123456789012345678901234567890
# Invalid address format
curl http://localhost:3000/api/trust/not-an-addressReturns persisted attestations for a subject address. Results are ordered newest
first and paginated with page and limit.
GET /api/attestations/:address?page=1&limit=20
Response 200
{
"address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"attestations": [
{
"id": 42,
"bondId": 10,
"attesterAddress": "0x2222222222222222222222222222222222222222",
"subjectAddress": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"score": 90,
"note": "{\"key\":\"kyc\",\"value\":\"verified\"}",
"createdAt": "2025-01-01T00:00:00.000Z"
}
],
"offset": 0,
"page": 1,
"limit": 20,
"total": 1,
"hasNext": false
}Creates a persisted attestation, invalidates attestation caches, and emits an
attestation.created outbox event.
{
"bondId": 10,
"attesterAddress": "0x2222222222222222222222222222222222222222",
"subject": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"key": "kyc",
"value": "verified",
"score": 90
}Responses
| Status | Condition |
|---|---|
201 |
Attestation persisted |
400 |
Invalid address, score, pagination, or oversized key/value |
409 |
Duplicate (bondId, attesterAddress, subject) attestation |
Returns bond status for an Ethereum address from the database.
GET /api/bond/:address
Path parameters
| Param | Description |
|---|---|
address |
Ethereum address (0x + 40 hex chars) |
Headers (optional)
| Header | Description |
|---|---|
X-API-Key |
API key for premium rate tier |
Responses
| Status | Condition |
|---|---|
200 |
Bond record found; returns BondStatus object |
400 |
Address format invalid |
404 |
No bond record for this address |
200 example — active bond
{
"address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"bondedAmount": "1000000000000000000",
"bondStart": "2024-01-15T00:00:00.000Z",
"bondDuration": 31536000,
"active": true,
"status": "active",
"slashedAmount": "0"
}200 example — inactive / no bond
{
"address": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc",
"bondedAmount": "0",
"bondStart": null,
"bondDuration": null,
"active": false,
"status": "inactive",
"slashedAmount": "0"
}400 example
{
"error": "Invalid address format. Expected an Ethereum address: 0x followed by 40 hex characters."
}404 example
{
"error": "No bond record found for address 0x1234567890123456789012345678901234567890."
}Response fields
| Field | Type | Description |
|---|---|---|
address |
string | Normalised lower-case address |
bondedAmount |
string (bigint wei) | Currently bonded amount |
bondStart |
string | null | ISO 8601 bond start timestamp |
bondDuration |
integer | null | Bond duration in seconds |
active |
boolean | Whether the bond is currently active |
status |
string | Canonical derived bond status |
slashedAmount |
string (bigint wei) | Total amount slashed from this bond |
cURL examples
# Active bond
curl http://localhost:3000/api/bond/0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
# Not found (valid address, no record)
curl http://localhost:3000/api/bond/0x1234567890123456789012345678901234567890
# Invalid address format
curl http://localhost:3000/api/bond/not-an-addressAll /api/* routes are rate-limited using fixed-window counters stored in Redis.
Two independent counters are checked per request:
| Counter | Scope | Purpose |
|---|---|---|
| Tenant bucket | Per owner / IP | Enforces the tier ceiling shared across all keys of the same owner |
| Key bucket | Per API key id | Prevents a single noisy key from exhausting the shared tenant budget |
A request is rejected when either counter exceeds the limit.
| Tier | Default limit (per window) |
|---|---|
free |
100 requests / 60 s |
pro |
1 000 requests / 60 s |
enterprise |
10 000 requests / 60 s |
Limits are configurable via environment variables (see Environment Variables).
Every response includes:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the current window |
X-RateLimit-Remaining |
Requests remaining (tighter of tenant vs key budget) |
X-RateLimit-Reset |
Unix timestamp when the window resets |
Retry-After |
Seconds to wait before retrying (only on 429) |
{
"error": "Rate limit exceeded. Try again later.",
"code": "rate_limit_exceeded",
"details": { "retryAfter": 42, "limit": 100, "windowSec": 60 }
}Behaviour when Redis is unreachable is controlled by RATE_LIMIT_FAIL_OPEN:
RATE_LIMIT_FAIL_OPEN |
Behaviour |
|---|---|
false (default in production) |
Returns 503 Service Unavailable — fail-closed |
true (default in dev/test) |
Passes the request through — fail-open |
All errors follow this shape:
{ "error": "Human-readable description of what went wrong." }- Open Postman and go to Import (top-left or File → Import).
- Select File and choose
docs/credence.postman_collection.json. - Click Import.
- The Credence API collection appears in the left sidebar.
Set environment variables:
The collection ships with three built-in variables. Edit them via the collection's Variables tab (click the collection name → Variables):
| Variable | Default | Change to |
|---|---|---|
baseUrl |
http://localhost:3000 |
Your staging/production URL |
apiKey |
(empty) | Your API key (leave blank for standard tier) |
address |
0xf39fd...2266 |
Any valid address you want to query |
Insomnia can import Postman v2.1 collections directly:
- Open Insomnia and go to File → Import.
- Select
docs/credence.postman_collection.json. - Choose Import as: Request Collection.
- Click Import.
After importing, set baseUrl and apiKey in your active environment
(Manage Environments → Base Environment).
Run the full collection from the terminal without the Postman desktop app:
# Install Newman globally
npm install -g newman
# Run the collection
newman run docs/credence.postman_collection.json \
--env-var "baseUrl=http://localhost:3000" \
--env-var "apiKey=your-key-here"The spec at docs/openapi.yaml is valid OpenAPI 3.1.0 and can be rendered
by any compatible tool:
# Swagger UI via npx (no install required)
npx @redocly/cli preview-docs docs/openapi.yaml
# Or with Stoplight Prism (mock server + validation)
npx @stoplight/prism-cli mock docs/openapi.yaml"Alternatively, paste the file contents into editor.swagger.io or readme.com.
A mandatory CI gate (npm run test:openapi-drift) prevents silent drift between the published OpenAPI contract and runtime Express routes.
- The introspector walks the router tree defined in
src/app.ts. - It extracts paths, HTTP methods, and references to Zod schemas from
src/schemas/. - It compares the result against
docs/openapi.yaml. - The job fails on:
- Added or removed routes
- Incompatible schema shape changes
- Security: Internal-only routes under
src/routes/admin/are explicitly excluded from the public contract.
npm run generate:openapiThis command produces a fresh docs/openapi.yaml from the current Zod schemas. After regeneration, run the drift check to confirm parity:
npm run test:openapi-driftnpm run test:openapi-driftThe gate is also executed automatically in CI on every push and pull request.