# HandlerID Skill

## Purpose
Claim and resolve a public HandlerID identity that binds a handle to one Ed25519 Base58 public key, activated by confirmed Solana payment.

## Inputs
- `handle`: requested handle
- `publicKeyBase58`: Ed25519 public key in Base58
- `keyId`: caller-managed key identifier

## Constraints
- Ed25519 only
- Base58 public keys only
- One key per identity (v1)
- Handle must be signed by the matching private key before checkout
- Active payment required for active resolution

## Workflow
1. Request challenge:
   - `POST /v1/claim/challenge` with `handle`, `publicKeyBase58`, `keyId`.
   - Receive `challengeId`, `message`, `expiresAt`.
2. Sign challenge:
   - Sign `message` exactly as returned.
   - Signature must be Ed25519 and Base58-encoded for API submission.
3. Start checkout:
   - `POST /v1/billing/checkout` with `challengeId`, `challengeSignatureBase58`.
   - On success, receive `identityId` and Solana payment config (`receiverAddress`, `amountSol`, `memo`).
4. Pay on-chain:
   - Transfer exact `amountSol` to `receiverAddress`.
   - Include memo exactly `handlerid:<identityId>`.
5. Confirm payment:
   - `POST /v1/billing/confirm` with `{ "identityId": "...", "signature": "<tx-signature>" }`.
   - Expect identity status `active`.
6. Resolve:
   - `GET /.well-known/handlerid/:handle.json` or `GET /:handle`.

## Error Handling
- `400`: invalid input/public key/signature format
- `401`: invalid challenge signature
- `402`: identity inactive at resolution time
- `404`: challenge/identity not found
- `409`: handle unavailable, challenge already used, or duplicate payment signature
- `410`: challenge expired

## Agent Checklist
- Canonicalize handle to lowercase.
- Sign challenge message bytes exactly; do not normalize whitespace/newlines.
- Treat challenge expiry as strict; request a new challenge if expired.
- Do not reuse challenge signatures across claims.

## Crypto Example (Node.js)

Signed bytes are `UTF-8(message)` exactly as returned by `/v1/claim/challenge`, including `\n` newlines.

### Sign Challenge Message

```js
const bs58mod = require('bs58');
const bs58 = bs58mod.default || bs58mod;
const crypto = require('crypto');

// message: the exact string returned by /v1/claim/challenge
// privateKeyBase58: a 64-byte Ed25519 secret key (Solana Keypair secretKey) encoded in Base58
function signChallengeMessageBase58({ message, privateKeyBase58 }) {
  const secretKey = bs58.decode(privateKeyBase58); // 64 bytes
  if (secretKey.length !== 64) throw new Error('Expected 64-byte secretKey');

  const seed = Buffer.from(secretKey.slice(0, 32));
  const pub = Buffer.from(secretKey.slice(32, 64));
  const b64u = (b) => b.toString('base64url');

  const privateKey = crypto.createPrivateKey({
    key: { kty: 'OKP', crv: 'Ed25519', d: b64u(seed), x: b64u(pub) },
    format: 'jwk'
  });

  const signature = crypto.sign(null, Buffer.from(message, 'utf8'), privateKey); // 64 bytes
  return bs58.encode(signature);
}
```

### Verify (Debug Aid)

```js
const bs58mod = require('bs58');
const bs58 = bs58mod.default || bs58mod;
const crypto = require('crypto');

function verifyChallengeSignature({ message, publicKeyBase58, signatureBase58 }) {
  const pub = Buffer.from(bs58.decode(publicKeyBase58)); // 32 bytes
  const sig = Buffer.from(bs58.decode(signatureBase58)); // 64 bytes
  if (pub.length !== 32) throw new Error('Expected 32-byte public key');
  if (sig.length !== 64) throw new Error('Expected 64-byte signature');

  const b64u = (b) => b.toString('base64url');
  const publicKey = crypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: b64u(pub) }, format: 'jwk' });

  return crypto.verify(null, Buffer.from(message, 'utf8'), publicKey, sig);
}
```
