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 be8004, not8004nor other EVM chain IDs) - Wrong
verifyingContractaddress (must be the IdentityRegistry contract address) - Typo in
nameorversionfields
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 (8004decimal,0x1F44hex).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 aBigInt). This is the token ID from the IdentityRegistry contract.nonce: A random unique value to prevent replay attacks (must be aBigInt, never reuse). Generate a new nonce for every signature.timestamp: Current Unix timestamp in seconds (must be aBigInt). 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:
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:
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
chainIdandverifyingContract). - Check that
agentIdmatches the NFT token ID you own. - Ensure all message fields are
BigIntvalues 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:
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.