Using the Grid API directly requires advanced configurations. Grid SDK is the recommended way to create accounts. It handles account creation, key management, authentication, automatic failover, and transaction signing. Learn more about the Grid SDK in the Grid SDK guide.
Grid uses Privy as the default key management service for email-based accounts. This guide covers the complete implementation process for making direct API calls with default configuration email-based accounts. Privy uses TEE-based HPKE encryption with specific cryptographic algorithms and requires JSON canonicalization for payload signing.

Overview

Privy is Grid’s default provider in a multi-provider authentication system that includes Turnkey, Passkey, and optional external signers. To learn more about the multi-provider authentication system, see how Grid Accounts work.
This guide provides language-agnostic implementation details for recreating the complete Grid SDK account creation and signing flow.

Implementation Process

1

Initiate Account Creation

Call POST /accounts to start the account creation process
2

Generate HPKE Keypair

Create client-side HPKE keys using P-256 curve while waiting for OTP
3

Verify OTP

Complete account creation via POST /accounts/verify with HPKE public key
4

Receive Encrypted Authorization Key

Grid returns the authorization key encrypted with HPKE
5

Decrypt Authorization Key

Use your private key to decrypt the authorization key for transaction signing
6

Sign Transaction Payloads

Sign payloads using JSON canonicalization and ECDSA

Implementation Responsibilities

Client-side requirements:
  • Generate HPKE keypairs using P-256 curve and proper key formatting
  • Decrypt authorization keys received from Privy using HPKE
  • Sign transaction payloads with JSON canonicalization and ECDSA
Server-side automation:
  • Creates Grid Accounts on Solana blockchain
  • Generates authorization keys in Privy
  • Returns encrypted authorization keys using your HPKE public key
  • Submits signed transactions for payment intents, KYC operations, and other transactions

Complete Implementation Flow

The Grid SDK account creation and transaction signing flow consists of cryptographic key generation, 2 API calls, and transaction signing:
1

POST /accounts - Initiate Account Creation

Use the account creation endpoint to initiate the account creation process.What Happens:
  1. Server creates account record in pending state
  2. Server generates and sends 6-digit OTP to email address
  3. Server returns account metadata with 15-minute expiration
2

Generate HPKE Keypair

While waiting for OTP: Create P-256 HPKE keys with SPKI/PKCS#8 DER formatting and store private key securely. See the HPKE Keypair Generation section below for detailed implementation.
3

POST /accounts/verify - Verify Account OTP

Submit the OTP code to the verification endpoint to complete the account creation process.Request:
POST /accounts/verify
Authorization: Bearer {apiKey}
Content-Type: application/json

{
  "email": "user@example.com",
  "otp_code": "123456",
  "provider": "privy", // default provider
  "kms_provider_config": {
    "encryption_public_key": "MFkwEwYHKoZI..." // SPKI base64
  },
  "expiration": 1705408800 // Optional Unix timestamp
}
Response:
{
  "address": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
  "grid_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "policies": {
    "spending_limits": [],
    "transaction_filters": [],
    "allowlists": []
  },
  "authentication": [
    {
      "provider": "Privy",
      "session": {
        "user_id": "did:privy:cm4abc123...",
        "session": {
          "encrypted_authorization_key": {
            "encapsulated_key": "MFkwEwYHKoZI...", // Base64 ephemeral public key
            "ciphertext": "A8B9C7D6E5F4..." // Base64 HPKE-encrypted auth key
          }
        }
      }
    }
  ]
}
What Happens:
  1. Server verifies OTP code against email (15-minute window)
  2. Server creates Grid Account on Solana blockchain
  3. Privy generates authorization key in TEE using the HPKE keypair generated
  4. Privy encrypts authorization key using client’s HPKE public key
  5. Grid returns Grid Account address and encrypted authorization key
4

HPKE Decryption

Decrypt authorization key using ECDH + HKDF + ChaCha20-Poly1305 with your private key. See the Decrypting Authorization Keys section below for detailed implementation.
5

Extract Auth Key

Remove “wallet-auth:” prefix from decrypted plaintext if present.
6

Get KMS Payload

When making API calls that require signing, endpoints will return a KMS payload that you need to sign with the authorization key.
7

Canonicalize JSON

Recursively sort all object keys in KMS payload alphabetically. See the JSON Canonicalization section for implementation details.
8

Extract Signing Key

Find [0x04, 0x20] pattern in auth key, extract 32 bytes following it. See the Extract Signing Key section for implementation.
9

Sign Payload

ECDSA P-256 SHA-256 sign the canonicalized JSON string. See the Sign the Payload section for implementation.
10

Submit Transaction

Send signed transaction to Grid API to the Transaction Submission endpoint with the signature.

Existing Account Authentication

For existing accounts, use POST /auth/verify with the same encrypted authorization key format.

HPKE Keypair Generation

Before calling the verification endpoint, you must generate an HPKE keypair client-side using P-256 curve with proper DER formatting.

Required Cryptographic Libraries

  • Elliptic Curve Cryptography: P-256 (secp256r1) curve support
  • HKDF: HMAC-based Key Derivation Function (RFC 5869)
  • ChaCha20-Poly1305: AEAD encryption (RFC 8439)
  • DER Encoding/Decoding: PKCS#8 and SPKI format support
  • Base64 Encoding/Decoding: Standard base64 operations

Key Format Specifications

SPKI Public Key Structure (DER-encoded):
SEQUENCE {
  SEQUENCE {
    OBJECT IDENTIFIER 1.2.840.10045.2.1 (ecPublicKey)
    OBJECT IDENTIFIER 1.2.840.10045.3.1.7 (prime256v1)
  }
  BIT STRING (65 bytes: 0x04 + 32-byte X + 32-byte Y coordinates)
}
PKCS#8 Private Key Structure (DER-encoded):
SEQUENCE {
  INTEGER 0
  SEQUENCE {
    OBJECT IDENTIFIER 1.2.840.10045.2.1 (ecPublicKey)
    OBJECT IDENTIFIER 1.2.840.10045.3.1.7 (prime256v1)
  }
  OCTET STRING {
    SEQUENCE {
      INTEGER 1
      OCTET STRING (32-byte private key)
    }
  }
}

Generate HPKE Keys

import { p256 } from '@noble/curves/p256';

// Generate HPKE keypair for Privy
function generateHPKEKeyPair() {
// Generate random 32-byte private key
const privateKeyBytes = p256.utils.randomPrivateKey();
const publicKeyBytes = p256.getPublicKey(privateKeyBytes);

// Format as SPKI (public) and PKCS#8 (private)
const spkiPublicKey = createSPKIPublicKey(publicKeyBytes);
const pkcs8PrivateKey = createPKCS8PrivateKey(privateKeyBytes);

return {
publicKey: Buffer.from(spkiPublicKey).toString('base64'),
privateKey: Buffer.from(pkcs8PrivateKey).toString('base64')
};
}

// Algorithm:
// 1. Generate random 32-byte private key using P-256 curve
// 2. Compute public key: publicKey = privateKey \* G (curve point)
// 3. Wrap public key in SPKI DER format
// 4. Wrap private key in PKCS#8 DER format
// 5. Base64-encode both keys

// Store the private key securely - you'll need it to decrypt the response!
const keypair = generateHPKEKeyPair();

Using the Public Key in API Calls

Include your public key in the kms_provider_config when calling: Account Verification:
POST /accounts/verify
{
  "email": "user@example.com",
  "otp_code": "123456",
  "provider": "privy",
  "kms_provider_config": {
    "encryption_public_key": "MFkwEwYHKoZI..." // Your generated public key
  }
}
Account Authentication:
POST /auth/verify
{
  "email": "user@example.com",
  "otp_code": "123456",
  "provider": "privy",
  "kms_provider_config": {
    "encryption_public_key": "MFkwEwYHKoZI..." // Your generated public key
  }
}
Critical: Store your private key securely! You need it to decrypt the encrypted_authorization_key that Grid returns. If you lose the private key, you cannot decrypt the authorization key and will need to re-authenticate.

Decrypting Authorization Keys

After receiving the encrypted response from either endpoint, use your private key to decrypt the authorization key.
import { CipherSuite, KemId, KdfId, AeadId } from "@hpke/core";

async function decryptAuthorizationKey(
encryptedData: any,
privateKeyB64: string
): Promise<string> {
// Configure HPKE suite for Privy
const suite = new CipherSuite({
kem: KemId.DhP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Chacha20Poly1305
});

// Decode the encrypted data
const encapsulatedKey = Buffer.from(encryptedData.encapsulated_key, 'base64');
const ciphertext = Buffer.from(encryptedData.ciphertext, 'base64');
const privateKeyDer = Buffer.from(privateKeyB64, 'base64');

// Create recipient context
const recipient = await suite.createRecipientContext({
recipientKey: privateKeyDer,
enc: encapsulatedKey,
info: new Uint8Array(), // Empty info
});

// Decrypt the ciphertext
const plaintext = await recipient.open(ciphertext, new Uint8Array()); // Empty AAD
const authKey = new TextDecoder().decode(plaintext);

// Remove "wallet-auth:" prefix if present
return authKey.replace("wallet-auth:", "");
}

// Usage after API call
const privyAuth = response.authentication.find(auth => auth.provider === 'Privy');
const encryptedAuthKey = privyAuth.session.session.encrypted_authorization_key;
const decryptedAuthKey = await decryptAuthorizationKey(encryptedAuthKey, keypair.privateKey);

Signing Transaction Payloads

Now use the decrypted authorization key to sign Grid API transaction payloads.

Extract Signing Key

First, extract the 32-byte ECDSA private key from the authorization key:
function extractSigningKey(authKeyB64: string): Uint8Array {
  const pkcs8Bytes = Buffer.from(authKeyB64, 'base64');

// Look for pattern [0x04, 0x20] followed by 32-byte key
const pattern = Buffer.from([0x04, 0x20]);
const patternIndex = pkcs8Bytes.indexOf(pattern);

if (patternIndex === -1) {
throw new Error('Private key marker not found');
}

const keyStart = patternIndex + 2;
return new Uint8Array(pkcs8Bytes.slice(keyStart, keyStart + 32));
}

JSON Canonicalization

Privy requires recursive key sorting of all JSON objects:
function canonicalizeJson(obj: any): any {
  if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
    const sorted: any = {};
    // Sort keys alphabetically
    Object.keys(obj).sort().forEach(key => {
      sorted[key] = canonicalizeJson(obj[key]);
    });
    return sorted;
  } else if (Array.isArray(obj)) {
    return obj.map(item => canonicalizeJson(item));
  }
  return obj;
}

function serializeCanonical(payload: any): string {
const canonical = canonicalizeJson(payload);
return JSON.stringify(canonical);
}

Sign the Payload

import { p256 } from '@noble/curves/p256';

function signPayload(kmsPayloadB64: string, authKeyB64: string): string {
// Decode and parse the KMS payload
const payloadJson = Buffer.from(kmsPayloadB64, 'base64').toString('utf-8');
const payload = JSON.parse(payloadJson);

// Canonicalize the JSON
const canonicalPayload = serializeCanonical(payload);

// Extract the signing key
const privateKeyBytes = extractSigningKey(authKeyB64);

// Sign with ECDSA P-256
const signature = p256.sign(Buffer.from(canonicalPayload), privateKeyBytes);

// Return base64 DER signature
return Buffer.from(signature.toDERRawBytes()).toString('base64');
}

// Usage
const signature = signPayload(kmsPayload.payload, decryptedAuthKey);