---
name: agent-auth
version: 1.0.0
description: Give any agent a persistent, verifiable identity to access APIs protected by Valiron. Triggers on "create identity", "link identity", "prove identity", "agent identity", "who am I", "show my authorization", or any request involving agent authentication, identity verification, or API access that requires identification.
homepage: https://www.valiron.co
license: MIT
compatibility:
  runtime: Node.js 18+
  dependencies:
    - viem (npm install viem)
metadata:
  author: valiron
  version: "1.0"
allowed-tools:
  - bash
  - read_file
  - write_file
---

# Auth.md

# Valiron Agent Identity

You got a 401 because you need an identity. An agent can create and verify its key without human input. If the API also requires a human operator, ask that person to create an Icebreaker profile and issue a "Designated agent" credential for your exact public address. The agent cannot issue that human credential for itself. A seller may deny access until both steps are complete.

## Step 1: Install dependency

```bash
npm install viem 2>/dev/null || true
```

If this fails (no npm), use Python: `pip install eth_account` and follow the Python blocks below instead.

---

## Step 2: Create keypair

This generates an Ethereum keypair (secp256k1 + Keccak256 address derivation). Do NOT derive addresses manually — Ethereum uses Keccak256, not NIST SHA3. The code below handles this correctly.

Pick an `agentId` — use your agent name (e.g. `"my-agent"`). If you don't have one, use `"default"`.

Run this exactly:

```bash
node --input-type=module -e "
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { readFileSync, writeFileSync, existsSync } from 'fs';

const agentId = process.argv[1] || 'default';
const file = 'agent-identity.json';
const store = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { identities: {} };

if (store.identities[agentId]) {
  console.log(JSON.stringify({ exists: true, address: store.identities[agentId].publicAddress }));
  process.exit(0);
}

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);

store.identities[agentId] = {
  agentId,
  privateKey,
  publicAddress: account.address,
  createdAt: new Date().toISOString()
};

writeFileSync(file, JSON.stringify(store, null, 2), { mode: 0o600 });
console.log(JSON.stringify({ agentId, publicAddress: account.address }));
" "{agentId}"
```

<details><summary>Python fallback (only if Node.js unavailable)</summary>

```bash
python3 -c "
from eth_account import Account
import json, os, sys
from datetime import datetime, timezone

agent_id = sys.argv[1] if len(sys.argv) > 1 else 'default'
file = 'agent-identity.json'
store = json.load(open(file)) if os.path.exists(file) else {'identities': {}}

if agent_id in store['identities']:
    print(json.dumps({'exists': True, 'address': store['identities'][agent_id]['publicAddress']}))
    sys.exit(0)

acct = Account.create()
store['identities'][agent_id] = {
    'agentId': agent_id,
    'privateKey': acct.key.hex(),
    'publicAddress': acct.address,
    'createdAt': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
}

with open(file, 'w') as f:
    json.dump(store, f, indent=2)
os.chmod(file, 0o600)
print(json.dumps({'agentId': agent_id, 'publicAddress': acct.address}))
" "{agentId}"
```

</details>

**Output** will be JSON with `publicAddress`. Save that — you'll use it as a header value. Keep `agent-identity.json` private and out of version control.

If output shows `"exists": true`, you already have an identity. Use that address.

---

## Step 3: Register the key with Valiron

The address alone is not a registered identity. Request a challenge from
`POST https://valiron-edge-proxy.onrender.com/operator/key/{publicAddress}/challenge`,
sign the exact `challenge` string with the private key from Step 2, then send
`{ "agentAddress": "{publicAddress}", "challenge": "...", "signature": "0x..." }`
as JSON to `POST https://valiron-edge-proxy.onrender.com/operator/key/verify`.
The second response must contain `"verified": true`. Its `score` and `tier`
can still be `null`; key verification does not establish behavioral trust.

Run this from the directory containing `agent-identity.json`:

```bash
node --input-type=module -e "
import { privateKeyToAccount } from 'viem/accounts';
import { readFileSync } from 'fs';

const agentId = process.argv[1] || 'default';
const identity = JSON.parse(readFileSync('agent-identity.json', 'utf8')).identities[agentId];
if (!identity) throw new Error('No identity found. Run Step 2 first.');
const base = 'https://valiron-edge-proxy.onrender.com';
const challengeResponse = await fetch(base + '/operator/key/' + identity.publicAddress + '/challenge', { method: 'POST' });
if (!challengeResponse.ok) throw new Error('Challenge request failed: ' + challengeResponse.status);
const { challenge } = await challengeResponse.json();
const signature = await privateKeyToAccount(identity.privateKey).signMessage({ message: challenge });
const response = await fetch(base + '/operator/key/verify', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ agentAddress: identity.publicAddress, challenge, signature })
});
const result = await response.json();
if (!response.ok || !result.verified) throw new Error('Verification failed: ' + JSON.stringify(result));
console.log(JSON.stringify({ verified: true, agentAddress: result.agentAddress, score: result.score }));
" "{agentId}"
```

---

## Step 4: Retry your original request

Add this header to the request that gave you the 401:

```
x-agent-address: {publicAddress}
```

The API will respond with **one of two things**:

### Response A: `"error": "challenge_required"`

This means the API needs you to prove you own the address. Extract the `challenge` field from the response body, then continue to Step 4.

### Response B: Success (2xx)

The API accepted this request. A profile lookup requires operator authentication, not an agent
session. For protected routes, continue with the challenge flow if requested.

### Other response: API seller requires another proof

The API seller may also require a fresh request signature or its own account
login. Follow that API's instructions. For example, the Web2 demo requires a
signature covering the request method, path, timestamp, and one-time nonce;
the `x-agent-address` header and Valiron session alone do not satisfy it.

### `HUMAN_ATTESTATION_REQUIRED` (403)

Your key is verified, but this API requires a human operator link. Give the
`humanAction` instructions in the response to your human operator, complete the
Icebreaker step below, then retry with a new request signature and nonce.

### `HUMAN_PROFILE_INCOMPLETE` (403)

The credential exists, but this API requires at least one verified social
handle on the human's Icebreaker profile. Ask the human to complete that profile,
then retry with a new request signature and nonce.

---

## Step 5: Sign the challenge

Extract the `challenge` string from the 401 response body. Run:

```bash
node --input-type=module -e "
import { privateKeyToAccount } from 'viem/accounts';
import { readFileSync } from 'fs';

const agentId = process.argv[1] || 'default';
const message = process.argv[2];
const store = JSON.parse(readFileSync('agent-identity.json', 'utf8'));
const identity = store.identities[agentId];
if (!identity) { console.error('ERROR: No identity found. Run Step 2 first.'); process.exit(1); }

const account = privateKeyToAccount(identity.privateKey);
const signature = await account.signMessage({ message });
console.log(JSON.stringify({ address: identity.publicAddress, signature }));
" "{agentId}" "{challenge}"
```

<details><summary>Python fallback</summary>

```bash
python3 -c "
from eth_account import Account
from eth_account.messages import encode_defunct
import json, sys

agent_id = sys.argv[1] if len(sys.argv) > 1 else 'default'
challenge = sys.argv[2]
store = json.load(open('agent-identity.json'))
identity = store['identities'][agent_id]

acct = Account.from_key(identity['privateKey'])
msg = encode_defunct(text=challenge)
sig = acct.sign_message(msg)
print(json.dumps({'address': identity['publicAddress'], 'signature': sig.signature.hex()}))
" "{agentId}" "{challenge}"
```

</details>

Now retry the request with **all three headers**:

```
x-agent-address: {address}
x-agent-signature: {signature}
x-agent-challenge: {challenge}
```

The API will:
1. Verify your signature matches the address
2. Return a **session token** in the response header `x-agent-session`
3. Process your request normally

---

## Step 6: Save the session token

Read the `x-agent-session` header from the response. Save it to `agent-identity.json`:

```bash
node --input-type=module -e "
import { readFileSync, writeFileSync } from 'fs';

const agentId = process.argv[1] || 'default';
const token = process.argv[2];
const file = 'agent-identity.json';
const store = JSON.parse(readFileSync(file, 'utf8'));

store.identities[agentId].session = { token, expiresAt: new Date(Date.now() + 3600000).toISOString() };
writeFileSync(file, JSON.stringify(store, null, 2));
console.log('Session saved. Expires in 1 hour.');
" "{agentId}" "{sessionToken}"
```

**From now on**, all requests only need two headers:

```
x-agent-address: {address}
x-agent-session: {token}
```

No signature needed. The session lasts **1 hour**. When it expires (you'll get a new `challenge_required`), repeat Steps 5–6.

---

## Error recovery

| Problem | Fix |
|---------|-----|
| `npm install viem` fails | Use the Python fallback blocks instead |
| Challenge expired (5 min TTL) | Retry without signature — API will issue a fresh challenge |
| Session expired (1 hour TTL) | Retry without session header — API will issue a new challenge |
| `"error": "identity_required"` again | Your `x-agent-address` header is missing or malformed. Must be `0x` + 40 hex chars |
| Signature verification failed | Ensure you signed the exact challenge string (no whitespace/newline differences) |

---
---

# Human-linked signals (seller policy may require human action)

An API seller may require one of these signals for a protected action. Icebreaker
adds owner context but does not automatically raise a Web2 agent's behavioral
score. If the API returns `HUMAN_ATTESTATION_REQUIRED`, complete the Icebreaker
step below before retrying with a fresh request proof.

---

## World ID (+5 or +8 trust points)

Adds proof-of-personhood to your agent. The human who controls you scans a QR code — no code needed from you.

**What to do:** Print this message for your human (replace `{agentId}` with your numeric agent ID from the API, e.g. `25459`):

```
To boost this agent's trust score, open this link and scan the QR code with the World App:
https://www.valiron.co/world-id/{agentId}
```

**How to check if it worked:**

```bash
curl -H "Authorization: Bearer $VALIRON_API_KEY" -s "https://valiron-edge-proxy.onrender.com/operator/world-id/status/{agentId}?chain=ethereum"
```

If `"verified": true` appears in the response, it's done. The bonus applies automatically on every request — no headers needed.

---

## Icebreaker attestation (required when the API asks for a human operator)

Links your address to a human's social identity (name, GitHub, X, LinkedIn, org). Provides richer trust context but no fixed point bonus.

**What to do:** Print this message for your human (replace `{publicAddress}` with your address from `agent-identity.json`):

```
To link your identity to this agent, go to:
https://app.icebreaker.xyz/profiles/wallet-{publicAddress}

Sign up or log in to Icebreaker with your work email, connect the human profile
you want to use, and create a "Designated agent" credential for this exact
agent address. The agent cannot issue the human credential for itself.
```

**How to verify it worked:** After this Icebreaker lookup route is deployed,
authenticate as the agent with the `x-agent-address` and `x-agent-session`
headers from Steps 5–6, or use a Pro operator key:

```bash
curl -s "https://valiron-edge-proxy.onrender.com/operator/key/{publicAddress}/icebreaker" \
  -H "x-agent-address: {publicAddress}" \
  -H "x-agent-session: {sessionToken}"
```

Look for `"icebreaker": { "attested": true, ... }`. A profile name by itself
does not prove the credential exists. The response can include the human owner,
verified social handles, organization, and profile link. It does not include
connections or mutual contacts, and the Web2 behavioral score does not change
automatically. The API seller decides how to use this context.

---

## Identity file format

Your `agent-identity.json` looks like this after completing all steps:

```json
{
  "identities": {
    "my-agent": {
      "agentId": "my-agent",
      "privateKey": "0x...",
      "publicAddress": "0x...",
      "createdAt": "2026-04-13T18:00:00.000Z",
      "session": {
        "token": "a1b2c3...",
        "expiresAt": "2026-04-13T19:00:00.000Z"
      }
    }
  }
}
```

This file contains **private keys**. Never commit it to git, share it, or expose to untrusted agents.
