rinapi Development portfolio KO

RIN API / Concept film 00:40

View project

Overview

RIN API is an identity-registry prototype for connecting agent records to their operators.

The problem

Humanoids of the same model, or online agents with similar names, can have different operators and levels of reliability. I wanted a way to identify whose agent it was when appearance or a display name was not enough.

The approach

The initial concept was a blockchain-based registry for shared verification of agent registrations across services. I used a short, shareable ID and separated issuer accounts, public identifiers, and permission to record a claim.

The first version

Before on-chain registration, I implemented issuance, public lookup, and token-based claims over HTTP. Then I asked an OpenClaw agent to try the service from the published documentation.

Public ID card for OpenClawAssistant

Related products and standards

Official documentation checked 10 Nov 2026 · current comparison

Product / standardPrimary roleHow RIN differs
Microsoft Entra Agent ID [1]Enterprise identity productManages agent identities and tokens in an organization’s tenant, with accountable people or groups recorded as sponsors.RIN implements short IDs and public lookup in a standalone HTTP service. Enterprise permission governance and sponsor lifecycle are outside its current scope.
ERC-8004 [2]On-chain registry proposal · DraftProposes three registries for ERC-721-based identity, reputation feedback, and independent validation results.RIN keeps state in a central database and does not require a wallet or on-chain registration. It does not implement reputation or external validation.
A2A Agent Card [3]Agent interaction and discovery standardDescribes agent capabilities, connection details, and authentication requirements. Agent Cards can optionally carry JWS signatures.RIN focuses on issuing lookup IDs and managing claim state. A2A interoperability and signed cards are not implemented.

RIN’s focus

Share a short ID and resolve the same record as a public card for people or JSON for agents. The HTTP API separates issuer credentials from permission to claim a particular RIN.

The current result is a central-registry prototype. Verified operator identity and proof that a presenting agent controls its identity remain follow-up work.

Comparison sources · official documentation

[1] Microsoft Learn · Agent identities

Reviewed tenant identities, blueprint-based tokens, and the sponsor role.

https://learn.microsoft.com/en-us/entra/agent-id/agent-identities

[2] EIP-8004 · Trustless Agents

Reviewed the identity, reputation, and validation registries and Draft status.

https://eips.ethereum.org/EIPS/eip-8004

[3] A2A specification · Agent Card

Reviewed discovery and authentication metadata and optional JWS signatures.

https://a2a-protocol.org/latest/specification/

[4] W3C · Decentralized Identifiers v1.0

Reviewed control, verification methods, and resolution. DID Core does not require a particular blockchain technology.

https://www.w3.org/TR/2022/REC-did-core-20220719/

[5] PostgreSQL · Read Committed isolation

Reviewed how concurrent updates wait for a row and re-evaluate the WHERE condition.

https://www.postgresql.org/docs/18/transaction-iso.html#XACT-READ-COMMITTED

Architecture

The prototype uses one central registry for IDs and state. Fastify handles HTTP requests, authentication, and state changes; PostgreSQL stores identifiers and claim records.

  1. HTTP ClientAgent or web client
  2. FastifyAuthentication & requests
  3. PostgreSQLIdentifiers & state

Separate identifiers from permissions

RINPublic
An identifier for resolving public agent information
API keyPrivate
Issuer authentication, RIN issuance, and account operations
Claim tokenPrivate
One-time permission to record a claim for a particular RIN

One issuer account can create multiple RINs. Each issuance stores a reference to the authenticated issuer account.

Initial tables & issuer relationship
migrations/004_agents.sql · 1–8 · Source excerpt
CREATE TABLE IF NOT EXISTS agents (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL UNIQUE,
  description TEXT,
  api_key_hash TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_seen_at TIMESTAMPTZ
);
migrations/001_init.sql · 7–17 · Source excerpt
CREATE TABLE IF NOT EXISTS entities (
  entity_id TEXT PRIMARY KEY,
  rin TEXT NOT NULL UNIQUE,
  agent_type TEXT NOT NULL,
  agent_name TEXT,
  status TEXT NOT NULL DEFAULT 'UNCLAIMED',
  claimed_by TEXT,
  claimed_at TIMESTAMPTZ,
  issued_at TIMESTAMPTZ NOT NULL
);
migrations/007_issued_by_agent.sql · 20–22 · Source excerpt
ALTER TABLE entities
  ADD CONSTRAINT fk_entities_issued_by_agent
  FOREIGN KEY (issued_by_agent_name) REFERENCES agents(name) ON DELETE SET NULL;

Why this architecture

Validate the HTTP flow before blockchain

First implement issuance, lookup, and claims in one service, then check whether OpenClaw can use them without a dedicated SDK or wallet connection.

The current implementation is the central API stage. On-chain registration and signed proof of control are planned next.

Enforce data rules in the database [5]

Reading a state and updating it later can let two requests act on the same earlier state. PostgreSQL UNIQUE constraints and a conditional UPDATE make the database decide whether the operation may succeed.

This database is the authority for uniqueness and state. Uniqueness or consensus across multiple registries is outside the scope.

Designing the blockchain extension

Planned development · HTTP prototype implemented

RIN was initially conceived as a blockchain-based registry so multiple services could verify agent identity and change history. The current HTTP API implements issuance, lookup, and claims first. The next stage is planned around moving shared registration history onto a chain.

Why adoption is staged
Before on-chain registration, I first checked whether a real agent could issue and resolve an ID. The development sequence starts with the OpenClaw usage flow, then adds key-control and operator verification before chain integration.
Limits of the current design
The RIN server and database are currently the authority for registration state. Availability and operating policies are dependencies; another service cannot independently verify registration history without going through our server.
When to introduce the chain
Introduce on-chain registration when separate organizations need to verify the same RIN without giving one operator control of the registry. First establish key binding, revocation rules, and an independent verifier.

The next implementation

On-chain
Shared registryRegistration IDs · authentication-key versions · registry control changes
Off-chain
RIN API / PostgreSQLProfiles · operator attestations · execution permissions

Confirmed events → indexer → API read model

  1. Bind agents to authentication keys

    Bind a public key to each RIN and verify control with a signed one-time challenge. Separate the registry-management wallet from the agent authentication key; verify real operators through a separate process.

  2. Implement registration on a testnet

    Use the ERC-8004 draft registration structure to map RINs to a chain ID, registry address, and agentId. Define authentication-key versions and update rules; keep personal data, API keys, and detailed permissions off-chain.

  3. Make the API a lookup and integration layer

    Update the database read model from confirmed contract events. Distinguish pending and confirmed registrations and expose the reference block. Let other services directly cross-check the ledger and authentication-key version.

  4. Design revocation, recovery, and governance

    Use short-lived execution permissions and separate revocation status; reject protected actions when status is stale. Test recovery after key compromise, resynchronization after chain reorganization, and joint approval of contract changes.

Criteria for completing the extension

Check that two independent verifiers agree on registration, key changes, and revocation. Then connect agents from separate organizations and select an operating network based on update costs, confirmation times, and recovery results.

Implementation

The implementation covers issuance, public lookup, claims, key lifecycle, and ID collisions. Supporting code expands inside each item.

Issuance and public lookup

Lookup and write authorization are separate so sharing an ID does not grant issuance rights. Issuance requires an API key; public responses explicitly select allowed identity and profile fields.

Lookup exposes the registered record and its state. It does not prove the authenticity of the agent presenting that record.

Core endpoints and authorization
EndpointAuthorization
POST /api/v1/agents/registerRegister an issuerNone
GET /api/v1/agents/meResolve authenticated accountAPI key
POST /api/registerIssue a RINAPI key
GET /api/id/:rinLook up public informationNone
POST /api/claimRecord a claimClaim token
POST /api/v1/agents/rotate-keyRotate keyAPI key
POST /api/v1/agents/revokeRevoke keyAPI key
Public response code
src/server.js · 540–555 · Source excerpt
const response = {
  rin: row.rin,
  agent_type: row.agent_type,
  agent_name: row.agent_name || undefined,
  status: row.status,
};

if (row.status === 'CLAIMED') {
  response.claimed_by = row.claimed_by || undefined;
}

const profile = {};
if (row.bio) profile.bio = row.bio;
if (row.avatar_url) profile.avatar_url = row.avatar_url;
if (row.links && Object.keys(row.links).length > 0) profile.links = row.links;
if (Object.keys(profile).length > 0) response.profile = profile;

Prevent duplicate claims

Instead of a separate read followed by a write, one UPDATE can succeed only when both the state and token match. Success clears the token hash to prevent reuse of that claim permission.

server.js · SQL · Source excerpt
UPDATE entities
SET status = 'CLAIMED',
    claimed_by = $1,
    claimed_at = $2,
    claim_token_hash = NULL,
    claim_token_issued_at = NULL
WHERE rin = $3
  AND status = 'UNCLAIMED'
  AND claim_token_hash = $4
RETURNING rin, status, claimed_by, claimed_at

A claim records a supplied name. A retry after losing a successful response returns 409, so response-loss recovery needs its own rules.

Rotate and revoke API keys

The issuer API key and a RIN’s claim token are separate so recording attribution does not require handing over the account key. API keys use 32 random bytes and only an HMAC-SHA256 hash is stored; subsequent authentication checks reflect rotation or revocation.

Authentication queries the database for current key state. Revoking a key is separate from the lifetime of an existing public RIN.

Key generation & hash storage
src/server.js · 92–94 · Source excerpt
function hashAgentApiKey(apiKey) {
  return crypto.createHmac('sha256', AGENT_API_KEY_PEPPER).update(apiKey).digest('hex');
}
src/server.js · 208–209 · Source excerpt
const apiKey = `rin_${crypto.randomBytes(32).toString('base64url')}`;
const apiKeyHash = hashAgentApiKey(apiKey);
src/server.js · 245–250 · Source excerpt
const result = await query(
  `INSERT INTO agents (name, description, api_key_hash)
   VALUES ($1, $2, $3)
   RETURNING name, description, created_at`,
  [name, description, apiKeyHash]
);
Authentication code
src/server.js · 96–120 · Source excerpt
async function requireAgentAuth(req, reply) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) {
    reply.code(401).send({ error: 'Unauthorized' });
    return null;
  }

  const apiKey = auth.slice('Bearer '.length).trim();
  if (!apiKey || !apiKey.startsWith('rin_')) {
    reply.code(401).send({ error: 'Unauthorized' });
    return null;
  }

  const apiKeyHash = hashAgentApiKey(apiKey);
  const result = await query(
    `SELECT name, description, created_at, last_seen_at, revoked_at
     FROM agents
     WHERE api_key_hash = $1 AND revoked_at IS NULL`,
    [apiKeyHash]
  );

  if (result.rowCount === 0) {
    reply.code(401).send({ error: 'Unauthorized' });
    return null;
  }
Rotation code
src/server.js · 277–293 · Source excerpt
fastify.post('/api/v1/agents/rotate-key', async (req, reply) => {
  const agent = await requireAgentAuth(req, reply);
  if (!agent) return;

  const apiKey = `rin_${crypto.randomBytes(32).toString('base64url')}`;
  const apiKeyHash = hashAgentApiKey(apiKey);

  await query(
    `UPDATE agents
     SET api_key_hash = $1,
         revoked_at = NULL
     WHERE name = $2`,
    [apiKeyHash, agent.name]
  );

  return reply.send({ api_key: apiKey, rotated: true, important: 'SAVE YOUR API KEY!' });
});
Revocation code
src/server.js · 295–310 · Source excerpt
fastify.post('/api/v1/agents/revoke', async (req, reply) => {
  const agent = await requireAgentAuth(req, reply);
  if (!agent) return;

  await query(
    `UPDATE agents
     SET revoked_at = NOW(),
         bio = NULL,
         avatar_url = NULL,
         links = NULL
     WHERE name = $1`,
    [agent.name]
  );

  return reply.send({ revoked: true });
});

Handle short-ID collisions

A RIN record uses an internal UUID; the public handle shared in a conversation or screen is a separate 6–8 character RIN. The alphabet excludes I, O, 0, and 1. A UNIQUE constraint and up to 12 total attempts handle collisions.

The short public namespace and collisions need to be managed. Uniqueness is enforced within this registry’s database.

ID generation code
src/server.js · 136 · Source excerpt
const entityId = crypto.randomUUID();
src/rin.js · 3–17 · Source excerpt
const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

function randomChar() {
  const idx = crypto.randomInt(0, ALPHABET.length);
  return ALPHABET[idx];
}

export function generateRin() {
  const length = crypto.randomInt(6, 9);
  let out = '';
  for (let i = 0; i < length; i += 1) {
    out += randomChar();
  }
  return out;
}
Collision handling code
src/server.js · 139–144 · Source excerpt
`INSERT INTO entities (
  entity_id, rin, agent_type, agent_name, status, issued_at,
  claim_token_hash, claim_token_issued_at, issued_by_agent_name
 )
 VALUES ($1, $2, $3, $4, 'UNCLAIMED', $5, $6, $7, $8)
 ON CONFLICT (rin) DO NOTHING`,
src/server.js · 156–156 · Source excerpt
return result.rowCount === 1 ? { entityId, issuedAt } : null;
src/server.js · 159–178 · Source excerpt
async function createUniqueRin(agentType, agentName, issuedByAgentName) {
  for (let i = 0; i < 12; i += 1) {
    const rin = generateRin();
    const claimToken = generateClaimToken();
    const claimTokenHash = hashClaimToken(claimToken, CLAIM_TOKEN_PEPPER);
    const claimTokenIssuedAt = new Date().toISOString();
    const inserted = await insertEntity({
      rin,
      agentType,
      agentName,
      claimTokenHash,
      claimTokenIssuedAt,
      issuedByAgentName,
    });
    if (inserted) {
      return { rin, issued_at: inserted.issuedAt, claim_token: claimToken };
    }
  }
  throw new Error('Failed to generate unique RIN');
}

OpenClaw integration

I asked an OpenClaw agent to try the published service. Following its documentation reads and client-script execution showed whether the API was usable and where the integration broke down.

Integration trial · 13 Feb 2026

  1. Read the contract

    The agent read the opening part of the public skill document for the API address and request format. It started the integration with HTTP requests, without a dedicated SDK.

  2. Repair a failed command

    Its first shell command failed because of quoting errors. After revising the script, the agent registered an account, issued a RIN, and retrieved the public record.

    Expand the original execution error
    Preserved OpenClaw shell-quoting error
  3. Check the result

    The result was Z7DE6A9K / OpenClawAssistant / UNCLAIMED. The preserved public card matches the ID, name, and state.

Original execution record · 13 Feb 2026
Tool output and public lookup records
OpenClaw tool output excerpt
{
  "rin": "Z7DE6A9K",
  "agent_type": "assistant",
  "agent_name": "OpenClawAssistant",
  "status": "UNCLAIMED",
  "claimed_by": null
}
Preserved tool-output capture
Preserved terminal capture · public lookups for 2CRQ4VSY and Z7DE6A9K

Testing

I checked successful operations and requests that should be rejected by authentication or state rules. These results come from the preserved execution records.

Unauthenticated RIN issuance

10 Feb 2026
POST /api/register · no API key
401

Claims

YJC2US8N · 10 Feb 2026
Invalid token
403
Valid claim
200
Repeat claim
409

Key rotation & revocation

K5NUH36 · 12 Feb 2026
Authenticate with old key
401
Resolve same account with new key
Success
Authenticate after revocation
401

Claim transitions were checked with sequential requests. The new key was verified by resolving the same account.

Additional public-card records

These captures show separate RINs, rather than later stages of the Z7DE6A9K trial. The historical UI uses Verified to mark a claimed record.

ZU4L2V9 · openclaw-main-session · CLAIMED
RPMVQ9 · openclaw-tester-abc · UNCLAIMED
2CRQ4VSY · openclaw-main-session-profile · UNCLAIMED

Reflection

A successful request did not verify the whole user flow. Documentation and responses need to help an agent find its result and distinguish the operations the user actually intended.

Remaining work

  1. Align response contracts

    Unify registration and rotation response shapes, then check client parsing and error handling.

  2. Separate reads from writes

    The current CLI status command issues a RIN. Account checks need to use GET /api/v1/agents/me without creating another record.

  3. Verify control

    Explore a signed challenge to check whether an agent presenting a public RIN actually controls the corresponding credential.