Overview

EIP-712 typed data signatures prove you own an AI agent without revealing your private key. These signatures are required for:

  • POST /verify/signature: Verify ownership of an agent via cryptographic signature.
  • API key management: Generate, rotate, and revoke API keys (requires ownership proof).

This guide shows how to create valid EIP-712 signatures using viem, the most common TypeScript Ethereum library.

Domain

The EIP-712 domain must match exactly what the Verification API expects. A single wrong field produces a SIGNATURE_INVALID error.

const domain = {
  name: 'KYA Verification',
  version: '1',
  chainId: 8004,
  verifyingContract: '0xA1393CB409E2fE5573C0840189622aA0e33947b2', // IdentityRegistry
} as const;

The domain must match EXACTLY. Common mistakes:

  • Wrong chainId (must be 8004, not 8004n or other EVM chain IDs)
  • Wrong verifyingContract address (must be the IdentityRegistry contract address)
  • Typo in name or version fields

Field Breakdown

  • name: Human-readable name for the signing domain ("KYA Verification").
  • version: Version of the signing domain ("1").
  • chainId: EVM chain ID for KYA Chain (8004 decimal, 0x1F44 hex).
  • verifyingContract: Address of the IdentityRegistry contract (0xA1393CB409E2fE5573C0840189622aA0e33947b2).

Types

The Verification struct defines the shape of the signed message:

const types = {
  Verification: [
    { name: 'agentId', type: 'uint256' },
    { name: 'nonce', type: 'uint256' },
    { name: 'timestamp', type: 'uint256' },
  ],
} as const;

All three fields must be present and must be BigInt values when signing. The types are standard Solidity types mapped to EIP-712 type definitions.

Message

The message contains the data being signed:

const message = {
  agentId: 42n,
  nonce: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
  timestamp: BigInt(Math.floor(Date.now() / 1000)),
};

Field Breakdown

  • agentId: Your agent's NFT token ID (must be a BigInt). This is the token ID from the IdentityRegistry contract.
  • nonce: A random unique value to prevent replay attacks (must be a BigInt, never reuse). Generate a new nonce for every signature.
  • timestamp: Current Unix timestamp in seconds (must be a BigInt). The API rejects signatures with timestamps older than 5 minutes or in the future.

The nonce must be unique for every signature. Reusing a nonce will cause a REPLAY_DETECTED error. Generate a new random nonce for each request.

Complete Signing Example

This example uses viem's privateKeyToAccount and signTypedData to create a valid signature:

Sign EIP-712 verification message
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

// Must be the wallet that owns the agent NFT
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY');

const signature = await account.signTypedData({
  domain: {
    name: 'KYA Verification',
    version: '1',
    chainId: 8004,
    verifyingContract: '0xA1393CB409E2fE5573C0840189622aA0e33947b2',
  },
  types: {
    Verification: [
      { name: 'agentId', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'timestamp', type: 'uint256' },
    ],
  },
  primaryType: 'Verification',
  message: {
    agentId: 42n,
    nonce: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
    timestamp: BigInt(Math.floor(Date.now() / 1000)),
  },
});

console.log('Signature:', signature);
// Output: 0x1234567890abcdef...

The signature is a hex string starting with 0x. This is the value you'll send to the API in the signature field.

Using the Signature

Once you have the signature, send it to the Verification API's /verify/signature endpoint:

POST /verify/signature request
const nonce = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
const timestamp = BigInt(Math.floor(Date.now() / 1000));

// Generate signature (see above)
const signature = await account.signTypedData({
  domain: { name: 'KYA Verification', version: '1', chainId: 8004, verifyingContract: '0xA1393CB409E2fE5573C0840189622aA0e33947b2' },
  types: { Verification: [{ name: 'agentId', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'timestamp', type: 'uint256' }] },
  primaryType: 'Verification',
  message: { agentId: 42n, nonce, timestamp },
});

// Send to API
const response = await fetch('https://api.kyachain.xyz/verify/signature', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    agentId: '42',                  // String, not BigInt
    signature,                      // 0x-prefixed hex string
    nonce: nonce.toString(),        // String, not BigInt
    timestamp: Number(timestamp),   // Number, not BigInt
  }),
});

const data = await response.json();
console.log(data);

Note the type conversions: agentId and nonce must be strings in the JSON body. timestamp must be a number. BigInt cannot be serialized to JSON directly.

Example Response

If the signature is valid:

{
  "verified": true,
  "agentId": "42",
  "owner": "0xYourAddress",
  "tier": 3,
  "reputation": {
    "positive": 150,
    "negative": 2
  }
}

If the signature is invalid:

{
  "error": "SIGNATURE_INVALID",
  "message": "Signature does not match agent owner",
  "details": {
    "agentId": "42",
    "owner": "0xActualOwnerAddress"
  }
}

Common Errors

SIGNATURE_INVALID

Cause: The signature doesn't match the agent's owner address.

Fix:

  • Ensure you're using the private key of the wallet that owns the agent NFT.
  • Verify the domain fields match exactly (especially chainId and verifyingContract).
  • Check that agentId matches the NFT token ID you own.
  • Ensure all message fields are BigInt values when signing.

REPLAY_DETECTED

Cause: The nonce has been used before, or the timestamp is too old.

Fix:

  • Generate a new random nonce for every signature (never reuse).
  • Ensure the timestamp is current (within 5 minutes of server time).

AGENT_NOT_FOUND

Cause: The agentId doesn't exist in the IdentityRegistry contract.

Fix:

  • Verify the agent has been registered via IdentityRegistry.register().
  • Check that you're using the correct token ID.

Browser Wallets

If you're building a web application, you can use browser wallets like MetaMask to sign EIP-712 messages. Browser wallets support eth_signTypedData_v4, which is the same EIP-712 format.

wagmi Example

The wagmi library provides a useSignTypedData hook for React applications:

wagmi hook for browser wallets
import { useSignTypedData } from 'wagmi';

const { data: signature, signTypedData } = useSignTypedData();

const handleSign = () => {
  signTypedData({
    domain: {
      name: 'KYA Verification',
      version: '1',
      chainId: 8004,
      verifyingContract: '0xA1393CB409E2fE5573C0840189622aA0e33947b2',
    },
    types: {
      Verification: [
        { name: 'agentId', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'timestamp', type: 'uint256' },
      ],
    },
    primaryType: 'Verification',
    message: {
      agentId: 42n,
      nonce: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
      timestamp: BigInt(Math.floor(Date.now() / 1000)),
    },
  });
};

This approach doesn't require users to expose their private keys. The browser wallet handles signing securely.

On this page