๐ก REST API Reference
All endpoints exposed by a running Soulprint validator node (default: http://localhost:4888).
GET /info
Returns node status and network statistics. No authentication required.
Response
{
"peerId": "12D3KooW...",
"version": "0.2.2",
"httpPort": 4888,
"p2pPort": 6888,
"connectedPeers": 3,
"attestationsIssued": 142,
"proofHashesStored": 891,
"uptime": 86400,
"multiaddrs": ["/ip4/0.0.0.0/tcp/6888/p2p/12D3KooW..."]
}
POST /verify
Verifies a Soulprint token and registers its proof hash for anti-replay. Returns the decoded token payload on success.
Request Body
{ "token": "<SPT token string>" }
Response (200 OK)
{
"valid": true,
"did": "did:soulprint:abc123...",
"score": 72,
"identity": 62,
"reputation": 10,
"country": "CO",
"credentials": ["email", "document", "face_match"],
"expiresAt": "2026-08-24T10:00:00Z",
"firstSeen": "2026-02-24T10:00:00Z"
}
Response (400 โ expired or invalid)
{ "valid": false, "error": "token expired" }
Response (409 โ replay detected)
{ "valid": false, "error": "proof hash already registered (Sybil attempt)" }
POST /reputation/attest
Issue a behavioral attestation for a DID. Requires operator authorization.
Request Body
{
"did": "did:soulprint:abc123...",
"delta": 1, // +1 (positive) or -1 (negative)
"reason": "diverse_tool_use", // free-form label for audit log
"operatorSig": "<signature>" // signed by operator key
}
Response (200 OK)
{
"ok": true,
"did": "did:soulprint:abc123...",
"newScore": 11,
"attestationId": "attest-uuid-..."
}
GET /reputation/:did
Returns the current bot reputation score for a DID.
Response
{
"did": "did:soulprint:abc123...",
"score": 12,
"attestations": 4, // total attestations received
"positive": 3,
"negative": 1,
"lastUpdated": "2026-02-24T14:00:00Z"
}
GET /proof-hash/:hash
Checks if a proof hash has been registered (anti-replay lookup).
Response
{
"hash": "sha3-abc123...",
"registered": true,
"firstSeen": "2026-02-24T10:00:00Z",
"did": "did:soulprint:abc123..." // included if registered
}
Credential Validators v0.3.0
POST /credentials/email/start
Send a 6-digit OTP to an email address.
// Request
{ "did": "did:soulprint:...", "email": "user@example.com" }
// Response
{ "sessionId": "abc123...", "message": "OTP sent", "preview": "https://ethereal.email/..." }
POST /credentials/email/verify
{ "sessionId": "abc123...", "otp": "123456" }
// Response
{ "credential": "EmailVerified", "did": "...", "attestation": { ... } }
POST /credentials/phone/start
Get a TOTP URI to scan with Google Authenticator / Authy / Aegis. No SMS.
{ "did": "...", "phone": "+573001234567" }
// Response
{ "sessionId": "...", "totpUri": "otpauth://totp/Soulprint:...", "instructions": "Scan QR..." }
POST /credentials/phone/verify
{ "sessionId": "...", "code": "179941" }
// Response
{ "credential": "PhoneVerified", "did": "...", "attestation": { ... } }
GET /credentials/github/start?did=...
Redirect to GitHub OAuth. Requires GITHUB_CLIENT_ID configured.
GET /credentials/github/callback
OAuth callback. Issues GitHubLinked attestation after successful auth.
{ "credential": "GitHubLinked", "did": "...", "github": { "login": "manuelariasfz" }, "attestation": { ... } }
Error Codes
| HTTP | Error | Meaning |
|---|---|---|
| 400 | invalid_token | Token is malformed or signature invalid |
| 400 | token_expired | Token has passed its expiresAt date |
| 401 | unauthorized | Missing or invalid operator signature for attestation endpoints |
| 409 | replay_detected | Proof hash already registered โ Sybil attempt blocked |
| 429 | rate_limited | Too many requests โ retry after Retry-After header seconds |
| 503 | node_syncing | Node is still syncing with the P2P network on startup |
Rate Limits
| Endpoint | Default limit | Notes |
|---|---|---|
GET /info | Unlimited | Health check endpoint |
POST /verify | 100/min per IP | Configurable via SOULPRINT_RATE_LIMIT |
POST /reputation/attest | 60/min per operator key | Requires valid operator signature |
GET /reputation/:did | 200/min per IP | Read-only, cached |
GET /proof-hash/:hash | 200/min per IP | Read-only, cached |
Blockchain Anchor
GET /anchor/stats
Returns the status of the async blockchain backup.
GET /anchor/stats
Response:
{
"nullifiersAnchored": 12, // total anchored to blockchain
"attestsAnchored": 47,
"pendingNullifiers": 0, // in retry queue
"pendingAttests": 0,
"blockchainConnected": true, // false = P2P-only mode
"lastAnchorTs": 1740000000000
}
Governance
On-chain governance for PROTOCOL_HASH upgrades via GovernanceModule.sol.
GET /governance
{
"currentApprovedHash": "0xdfe1ccca...",
"blockchainConnected": true,
"activeProposals": 0,
"hashHistory": ["0xdfe1ccca..."],
"nodeCompatible": true
}
GET /governance/proposals
List all active or approved-pending-timelock proposals.
GET /governance/proposal/:id
Full proposal detail including timelockRemainingSeconds.
POST /governance/propose
{ "did": "did:key:z6Mk...", "newHash": "0x...", "rationale": "..." }
โ { "txHash": "0x...", "proposalId": 0 }
POST /governance/vote
{ "proposalId": 0, "did": "did:key:z6Mk...", "approve": true }
โ { "txHash": "0x...", "proposalId": 0, "approve": true }
POST /governance/execute
{ "proposalId": 0 }
โ { "txHash": "0x...", "proposalId": 0, "executed": true }
POST /challenge v0.3.7
Peer integrity check via ZK challenge-response. Verifies that the peer is running unmodified ZK verification code.
POST /challenge
Content-Type: application/json
{
"challenge_id": "a3f1b2c4...", // random UUID
"nonce": "deadbeef01234567...", // 32-byte hex
"issued_at": 1740000000, // unix timestamp (TTL: 30s)
"valid_proof": { ... }, // ZKProof โ must verify as true
"invalid_proof": { ... } // ZKProof mutated with nonce โ must verify as false
}
// Response 200
{
"challenge_id": "a3f1b2c4...",
"result_valid": true,
"result_invalid": false,
"verified_at": 1740000002,
"node_did": "did:key:z6Mk...",
"signature": "ed25519:abc..."
}
// Response 400 โ missing fields or expired challenge
// Response 403 โ peer rejected (used in POST /peers/register flow)
POST /token/renew v0.3.6
Auto-renew an SPT that is near expiry (< 1h remaining) or recently expired (grace window: 7 days).
POST /token/renew
Content-Type: application/json
{ "spt": "<current_spt_string>" }
// Response 200
{
"spt": "<new_spt_string>",
"expires_in": 86400,
"renewed": true,
"method": "preemptive", // or "grace_window"
"old_expired": false,
"node_did": "did:key:z6Mk..."
}
// Response 400 โ token still valid (> 1h remaining), includes renew_after
// Response 401 โ token expired > 7 days (full re-verification required)
// Response 403 โ DID not registered or score below floor
// Response 429 โ cooldown (60s between renewals per DID)
DPoP โ Demonstrating Proof of Possession v0.3.8
Prevents stolen SPT abuse. Every request must carry a fresh signed proof. Without the private key, a stolen token is useless.
Header: X-Soulprint-Proof: <base64url-proof>
// Proof payload (Ed25519 signed):
{
"typ": "soulprint-dpop",
"method": "POST", // HTTP method โ must match request
"url": "https://...", // exact URL โ must match request
"nonce": "a3f1b2...", // 16 random bytes hex โ unique per request
"iat": 1740000000, // Unix timestamp โ expires in 300s
"spt_hash": "sha256(spt)" // bound to this specific token
}
// Serialized: base64url(JSON.stringify({ payload, signature, did }))
// Generate (client-side):
import { signDPoP, serializeDPoP } from "soulprint-core";
const proof = signDPoP(privateKey, did, "POST", url, myToken);
const header = serializeDPoP(proof);
// Enable on server (soulprint-express):
app.use(soulprint({ minScore: 65, requireDPoP: true }));
// โ 401 { error: "dpop_required" } if proof header is missing
// Enable on MCP (soulprint-mcp):
server.use(soulprint({ minScore: 65, requireDPoP: true }));
// Attacks blocked: token theft ยท replay ยท URL MITM ยท method MITM ยท
// DID mismatch ยท spt_hash mismatch ยท expired proof ยท malformed proof
MCPRegistry โ On-Chain Verified MCPs v0.3.9
Public registry of verified MCP servers on Base Sepolia. Agents can verify a server is legitimate before trusting it.
Contract: 0x59EA3c8f60ecbAe22B4c323A8dDc2b0BCd9D3C2a (Base Sepolia)
GET /mcps/verified
List all currently verified MCPs. Public, no authentication required.
GET /mcps/verified
// Response 200
{
"total": 1,
"registry": {
"contract": "0x59EA3c8f60ecbAe22B4c323A8dDc2b0BCd9D3C2a",
"network": "Base Sepolia (chainId: 84532)",
"superAdmin": "0x0755...",
"totalMCPs": 1,
"explorer": "https://sepolia.basescan.org/address/0x59EA..."
},
"mcps": [
{
"address": "0x...",
"name": "MCP Colombia Hub",
"url": "https://npmjs.com/package/mcp-colombia-hub",
"category": "general",
"description": "...",
"verified_at": "2026-02-25T01:02:24.000Z",
"badge": "โ
VERIFIED"
}
]
}
GET /mcps/all
List all registered MCPs (verified + pending). Public.
GET /mcps/status/:address
Check verification status of a specific MCP address.
GET /mcps/status/0x0755A3001F488da00088838c4a068dF7f883ad87
// Response 200
{
"address": "0x...",
"name": "MCP Colombia Hub",
"registered": true,
"verified": true,
"registered_at": "2026-02-25T01:02:22.000Z",
"verified_at": "2026-02-25T01:02:24.000Z",
"revoked_at": null,
"badge": "โ
VERIFIED by Soulprint"
}
// Response 404 โ not registered
{ "address": "0x...", "registered": false, "verified": false }
POST /admin/mcp/register (permissionless)
Register a new MCP on-chain. Anyone can register; verification requires admin approval.
POST /admin/mcp/register
Content-Type: application/json
{
"ownerKey": "0x<private_key>", // signs the on-chain tx
"address": "0x<mcp_address>",
"name": "My Finance MCP",
"url": "https://my-mcp.example.com",
"category": "finance", // finance|travel|jobs|ecommerce|general
"description": "Provides Colombian financial data"
}
// Response 201
{
"address": "0x...",
"name": "My Finance MCP",
"txHash": "0x...",
"explorer": "https://sepolia.basescan.org/tx/0x...",
"message": "โ
MCP registered. Contact Soulprint admin to verify.",
"next_step": "POST /admin/mcp/verify with Bearer ADMIN_TOKEN"
}
POST /admin/mcp/verify ๐ Admin only
Mark an MCP as verified on-chain. Requires Authorization: Bearer ADMIN_TOKEN + server-side ADMIN_PRIVATE_KEY.
POST /admin/mcp/verify
Authorization: Bearer <ADMIN_TOKEN>
Content-Type: application/json
{ "address": "0x<mcp_address>" }
// Response 200
{
"address": "0x...",
"verified": true,
"txHash": "0x...",
"explorer": "https://sepolia.basescan.org/tx/0x...",
"message": "โ
MCP 0x... verified on-chain by Soulprint"
}
// Response 401 โ missing or wrong ADMIN_TOKEN
// Response 503 โ ADMIN_PRIVATE_KEY not configured on this node
POST /admin/mcp/revoke ๐ Admin only
Revoke an MCP's verification. The reason is stored permanently on-chain.
POST /admin/mcp/revoke
Authorization: Bearer <ADMIN_TOKEN>
Content-Type: application/json
{ "address": "0x<mcp_address>", "reason": "Malicious behavior detected" }
// Response 200
{
"address": "0x...",
"revoked": true,
"reason": "Malicious behavior detected",
"txHash": "0x...",
"message": "๐ซ MCP 0x... revoked. Reason: 'Malicious behavior detected'"
}
ProtocolThresholds โ On-Chain Governance v0.4.1
Protocol thresholds (SCORE_FLOOR, FACE_SIM_*, etc.) live on ProtocolThresholds.sol (Base Sepolia: 0xD8f78d65b35806101672A49801b57F743f2D2ab1). Only the superAdmin can modify them; anyone can read. Validators load them at startup and refresh every 10 minutes.
GET /protocol/thresholds
Returns all live protocol thresholds loaded from the blockchain.
GET /protocol/thresholds
Response 200
{
"source": "blockchain",
"contract": "0xD8f78d65b35806101672A49801b57F743f2D2ab1",
"chain": "Base Sepolia (chainId: 84532)",
"thresholds": {
"SCORE_FLOOR": 65,
"VERIFIED_SCORE_FLOOR": 52,
"MIN_ATTESTER_SCORE": 65,
"FACE_SIM_DOC_SELFIE": 0.35,
"FACE_SIM_SELFIE_SELFIE": 0.65,
"DEFAULT_REPUTATION": 10,
"IDENTITY_MAX": 80,
"REPUTATION_MAX": 20
},
"last_loaded": "2026-02-24T22:00:00.000Z",
"note": "Solo el superAdmin del contrato puede modificar estos valores on-chain"
}
source: "blockchain" when loaded from chain, "local_fallback" if RPC is unreachable at node startup.
On-Chain Read (Solidity / ethers.js)
const ABI = [
"function getThreshold(string calldata name) external view returns (uint256)",
"function getAll() external view returns (string[] memory names, uint256[] memory values)",
];
const contract = new ethers.Contract(
"0xD8f78d65b35806101672A49801b57F743f2D2ab1", ABI, provider
);
const scoreFloor = await contract.getThreshold("SCORE_FLOOR"); // โ 65n
const [names, values] = await contract.getAll(); // โ 9 entries
SuperAdmin โ Update Threshold
Only callable by the superAdmin wallet. Emits ThresholdUpdated(key, oldValue, newValue, by, timestamp).
// Only superAdmin
const ABI_WRITE = [
"function setThreshold(string calldata name, uint256 value) external",
];
const c = new ethers.Contract(CONTRACT_ADDRESS, ABI_WRITE, adminWallet);
await (await c.setThreshold("SCORE_FLOOR", 70)).wait();
Canonical Values (v0.4.1)
| Name | Value | Notes |
|---|---|---|
SCORE_FLOOR | 65 | Min score for protected services |
VERIFIED_SCORE_FLOOR | 52 | Min combined identity+reputation |
MIN_ATTESTER_SCORE | 65 | Min score to issue attestations |
FACE_SIM_DOC_SELFIE | 350 (0.35) | Doc-photo vs selfie cosine similarity ร1000 |
FACE_SIM_SELFIE_SELFIE | 650 (0.65) | Selfie vs selfie ร1000 |
DEFAULT_REPUTATION | 10 | Initial reputation for new identities |
IDENTITY_MAX | 80 | Max identity contribution to score |
REPUTATION_MAX | 20 | Max reputation contribution to score |