Skip to main content

L{CORE} dApp Integration Guide

How to integrate your dApp with L{CORE} to access user attestation data.

Prerequisites: Your dApp must be registered with the Attestor service and have a valid API key.


Table of Contents​

  1. Overview
  2. Authentication
  3. User Grant Flow
  4. Querying Attestations
  5. Verifying Decryption Proofs
  6. Aggregate Queries
  7. SDK Usage
  8. Error Handling
  9. Best Practices
  10. Example Implementations

Overview​

L{CORE} is a privacy-preserving attestation data layer. As a dApp developer, you can:

  1. Request access to user attestation data (with user consent)
  2. Query attestations you've been granted access to
  3. Access aggregates for anonymous statistical insights

Architecture​

dApp Integration Architecture
YOUR DAPP
Frontend
React, Vue, etc
Backend
Server
Smart Contracts
On-chain
API Key Authentication
ATTESTOR SERVICE
  • Validates API keys
  • Verifies grants
  • Decrypts L{CORE} data
  • Returns plaintext to authorized dApps
Encrypted communication
L{CORE}
  • Stores attestation data (encrypted)
  • Manages access grants
  • Provides aggregate statistics

Authentication​

API Key Setup​

  1. Register your dApp with the Attestor admin
  2. Receive an API key (format: dapp_key_xxxxxxxx)
  3. Store securely - treat like a password

Using API Keys​

Include your API key in the X-API-Key header:

curl -X GET https://attestor.example.com/api/lcore/dapp/grants \
-H "X-API-Key: dapp_key_your_api_key_here"

TypeScript Example​

const ATTESTOR_URL = 'https://attestor.example.com';
const API_KEY = process.env.LCORE_API_KEY;

async function lcoreRequest(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`${ATTESTOR_URL}${endpoint}`, {
...options,
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
...options.headers,
},
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}

return response.json();
}

User Grant Flow​

Before you can access a user's attestation data, they must grant you permission.

Flow Overview​

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│ User │ │ Your │ │ Attestor │ │ L\{CORE\} │
│ │ │ dApp │ │ │ │ │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ 1. "Connect │ │ │
│ attestations" │ │ │
│───────────────►│ │ │
│ │ │ │
│ 2. Request │ │ │
│ signature │ │ │
│◄───────────────│ │ │
│ │ │ │
│ 3. Sign │ │ │
│ grant message │ │ │
│───────────────►│ │ │
│ │ │ │
│ │ 4. Submit │ │
│ │ signed grant │ │
│ │───────────────►│ │
│ │ │ │
│ │ │ 5. Verify & │
│ │ │ store grant │
│ │ │───────────────►│
│ │ │ │
│ │ 6. Grant ID │ │
│ │◄───────────────│ │
│ │ │ │
│ 7. Access │ │ │
│ granted │ │ │
│◄───────────────│ │ │
│ │ │ │

Step 1: Get User's Attestations​

First, show the user what attestations they have available:

interface UserAttestation {
id: string;
provider: string;
flowType: string;
status: 'active' | 'revoked' | 'expired';
createdAt: string;
validUntil: string;
}

async function getUserAttestations(
walletAddress: string,
signature: string,
message: string
): Promise<UserAttestation[]> {
const response = await fetch(`${ATTESTOR_URL}/api/lcore/user/attestations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Wallet-Address': walletAddress,
'X-Signature': signature,
'X-Message': message,
},
});

const data = await response.json();
return data.attestations;
}

Step 2: Create Grant Message​

Generate a message for the user to sign:

interface GrantRequest {
attestationId: string;
granteeAddress: string; // Your dApp's registered address
grantType: 'full' | 'partial' | 'aggregate';
dataKeys?: string[]; // For partial grants
expiresInDays: number;
}

function createGrantMessage(request: GrantRequest): string {
const timestamp = Date.now();
const nonce = crypto.randomUUID();

return JSON.stringify({
action: 'grant_access',
attestation_id: request.attestationId,
grantee: request.granteeAddress,
grant_type: request.grantType,
data_keys: request.dataKeys || null,
expires_in_days: request.expiresInDays,
timestamp,
nonce,
});
}

Step 3: Request User Signature​

// Using ethers.js
import { BrowserProvider } from 'ethers';

async function requestGrantSignature(message: string): Promise<{
signature: string;
address: string;
}> {
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();

const signature = await signer.signMessage(message);

return { signature, address };
}

Step 4: Submit Grant​

interface GrantResponse {
grantId: string;
attestationId: string;
expiresAt: string;
}

async function submitGrant(
walletAddress: string,
signature: string,
message: string,
request: GrantRequest
): Promise<GrantResponse> {
const response = await fetch(`${ATTESTOR_URL}/api/lcore/user/grants`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Wallet-Address': walletAddress,
'X-Signature': signature,
'X-Message': message,
},
body: JSON.stringify(request),
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}

return response.json();
}

Querying Attestations​

Once you have a grant, you can query the attestation data.

Get Single Attestation​

interface AttestationData {
id: string;
ownerAddress: string;
provider: string;
flowType: string;
domain: string;
validFrom: string;
validUntil: string;
freshnessScore: number;
buckets: Record<string, string>; // e.g., { balance: "25k-50k" }
data?: {
parameters?: string;
context?: string;
};
}

async function getAttestation(attestationId: string): Promise<AttestationData> {
return lcoreRequest(`/api/lcore/dapp/attestation/${attestationId}`);
}

Get All Your Grants​

interface Grant {
grantId: string;
attestationId: string;
ownerAddress: string;
grantType: 'full' | 'partial' | 'aggregate';
dataKeys: string[] | null;
expiresAt: string;
status: 'active' | 'expired' | 'revoked';
}

async function getMyGrants(): Promise<Grant[]> {
const response = await lcoreRequest('/api/lcore/dapp/grants');
return response.grants;
}

Batch Query Attestations​

async function getMultipleAttestations(
attestationIds: string[]
): Promise<AttestationData[]> {
const response = await lcoreRequest('/api/lcore/dapp/attestations/batch', {
method: 'POST',
body: JSON.stringify({ attestation_ids: attestationIds }),
});
return response.attestations;
}

Verifying Decryption Proofs​

When the Attestor returns data from L{CORE}, it includes a decryption proof if the data was encrypted. This proof allows your dApp to cryptographically verify that:

  1. The data was decrypted by a trusted TEE (Trusted Execution Environment)
  2. The ciphertext hash matches what was received from L{CORE}
  3. The plaintext hash matches the returned data

Response Format with Proof​

interface LCoreQueryResult<T> {
data: T;
wasEncrypted: boolean;
proof?: DecryptionProof;
}

interface DecryptionProof {
/** SHA256 hash of the encrypted payload (hex) */
ciphertextHash: string;
/** SHA256 hash of the decrypted plaintext JSON (hex) */
plaintextHash: string;
/** Unix timestamp when decryption occurred */
timestamp: number;
/** TEE/Attestor wallet address that performed decryption */
teeAddress: string;
/** ECDSA signature over keccak256(ciphertextHash, plaintextHash, timestamp) */
signature: string;
}

Why Verify Proofs?​

Without proof verification, you're trusting the Attestor service blindly. By verifying the decryption proof, you can:

  • Ensure data integrity: The plaintext wasn't modified after decryption
  • Verify TEE authenticity: The decryption was performed by a known, trusted TEE
  • Audit trail: Keep records of who decrypted what and when

Verifying Proofs (TypeScript)​

import { utils } from 'ethers';

interface DecryptionProof {
ciphertextHash: string;
plaintextHash: string;
timestamp: number;
teeAddress: string;
signature: string;
}

/**
* Verify a decryption proof from the Attestor.
*
* @param proof - The decryption proof to verify
* @param trustedTeeAddresses - List of TEE addresses you trust
* @returns true if proof is valid and from a trusted TEE
*/
function verifyDecryptionProof(
proof: DecryptionProof,
trustedTeeAddresses: string[]
): boolean {
try {
// Recreate the message hash that was signed
const messageHash = utils.keccak256(
utils.solidityPack(
['bytes32', 'bytes32', 'uint256'],
[proof.ciphertextHash, proof.plaintextHash, proof.timestamp]
)
);

// Recover the signer's address from the signature
const recoveredAddress = utils.verifyMessage(
utils.arrayify(messageHash),
proof.signature
);

// Check if recovered address matches the claimed TEE address
if (recoveredAddress.toLowerCase() !== proof.teeAddress.toLowerCase()) {
console.error('Signature does not match claimed TEE address');
return false;
}

// Check if the TEE is in our trusted list
const isTrusted = trustedTeeAddresses.some(
addr => addr.toLowerCase() === proof.teeAddress.toLowerCase()
);

if (!isTrusted) {
console.error('TEE address not in trusted list:', proof.teeAddress);
return false;
}

// Optionally check timestamp is recent (e.g., within last hour)
const oneHourAgo = Math.floor(Date.now() / 1000) - 3600;
if (proof.timestamp < oneHourAgo) {
console.warn('Decryption proof is older than 1 hour');
// Depending on your use case, you may want to reject old proofs
}

return true;
} catch (error) {
console.error('Proof verification failed:', error);
return false;
}
}

Full Example with Proof Verification​

import { LCoreClient } from '@citychain/lcore-sdk';

const lcore = new LCoreClient({
attestorUrl: process.env.ATTESTOR_URL,
apiKey: process.env.LCORE_API_KEY,
});

// Trusted TEE addresses (get from Attestor registry)
const TRUSTED_TEES = [
'0x1234567890abcdef1234567890abcdef12345678',
'0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
];

async function getVerifiedAttestation(attestationId: string) {
const result = await lcore.getAttestation(attestationId);

// If data was encrypted, verify the decryption proof
if (result.wasEncrypted && result.proof) {
const isValid = verifyDecryptionProof(result.proof, TRUSTED_TEES);

if (!isValid) {
throw new Error('Decryption proof verification failed');
}

console.log('Decryption proof verified successfully');
console.log('Decrypted by TEE:', result.proof.teeAddress);
console.log('Decrypted at:', new Date(result.proof.timestamp * 1000));
}

return result.data;
}

On-Chain Proof Verification (Solidity)​

For high-security use cases, you can verify proofs on-chain:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract DecryptionProofVerifier {
using ECDSA for bytes32;

mapping(address => bool) public trustedTees;

event ProofVerified(
bytes32 indexed ciphertextHash,
bytes32 indexed plaintextHash,
address indexed teeAddress,
uint256 timestamp
);

function verifyDecryptionProof(
bytes32 ciphertextHash,
bytes32 plaintextHash,
uint256 timestamp,
address teeAddress,
bytes memory signature
) external view returns (bool) {
// Recreate the message hash
bytes32 messageHash = keccak256(
abi.encodePacked(ciphertextHash, plaintextHash, timestamp)
);

// Convert to Ethereum signed message hash
bytes32 ethSignedHash = messageHash.toEthSignedMessageHash();

// Recover signer
address recoveredSigner = ethSignedHash.recover(signature);

// Verify signer matches claimed TEE and is trusted
return recoveredSigner == teeAddress && trustedTees[teeAddress];
}
}

Best Practices for Proof Verification​

  1. Always verify for sensitive operations: Loan approvals, access grants, etc.
  2. Maintain a trusted TEE registry: Keep an updated list of trusted TEE addresses
  3. Check proof freshness: Reject proofs older than your security threshold
  4. Log verification results: Keep audit trails for compliance
  5. Handle verification failures gracefully: Don't expose internal errors to users

Aggregate Queries​

Aggregate queries don't require grants - they return anonymized statistics.

Count by Bucket​

interface BucketCount {
bucketValue: string;
count: number;
}

interface AggregateResponse {
bucketKey: string;
counts: BucketCount[];
total: number;
kAnonymityThreshold: number;
}

async function getCountByBucket(
domain: string,
bucketKey: string,
provider?: string
): Promise<AggregateResponse> {
const params = new URLSearchParams({ domain, bucket_key: bucketKey });
if (provider) params.append('provider', provider);

// No API key required for aggregates
const response = await fetch(
`${ATTESTOR_URL}/api/lcore/aggregates/count-by-bucket?${params}`
);
return response.json();
}

// Example usage
const balanceDistribution = await getCountByBucket('lending', 'balance', 'chase');
// Returns:
// {
// bucketKey: "balance",
// counts: [
// { bucketValue: "<1k", count: 123 },
// { bucketValue: "1k-5k", count: 456 },
// { bucketValue: "5k-10k", count: 234 },
// ...
// ],
// total: 1000,
// kAnonymityThreshold: 5
// }

Count by Provider​

interface ProviderCount {
provider: string;
flowType: string;
count: number;
}

async function getCountByProvider(domain: string): Promise<ProviderCount[]> {
const response = await fetch(
`${ATTESTOR_URL}/api/lcore/aggregates/count-by-provider?domain=${domain}`
);
const data = await response.json();
return data.counts;
}

K-Anonymity​

Aggregate results are protected by k-anonymity:

  • Buckets with fewer than k users are suppressed
  • Default threshold is k=5
  • This prevents identifying individuals
// Example: A bucket with 3 users won't be shown
// Instead, you'll see: { bucketValue: "hidden", count: "< 5" }

SDK Usage​

For TypeScript/JavaScript projects, use the L{CORE} SDK.

Installation​

npm install @citychain/lcore-sdk

Initialization​

import { LCoreClient } from '@citychain/lcore-sdk';

const lcore = new LCoreClient({
attestorUrl: 'https://attestor.example.com',
apiKey: process.env.LCORE_API_KEY,
});

Full Example​

import { LCoreClient, GrantType } from '@citychain/lcore-sdk';

const lcore = new LCoreClient({
attestorUrl: process.env.ATTESTOR_URL,
apiKey: process.env.LCORE_API_KEY,
});

// Get user's attestations (requires user signature)
const attestations = await lcore.getUserAttestations({
walletAddress: userAddress,
signature: userSignature,
message: signedMessage,
});

// Request a grant
const grant = await lcore.requestGrant({
attestationId: attestations[0].id,
grantType: GrantType.Full,
expiresInDays: 30,
userSignature: grantSignature,
userAddress: userAddress,
});

// Query the attestation data
const data = await lcore.getAttestation(grant.attestationId);

console.log('User balance bucket:', data.buckets.balance);
// Output: "25k-50k"

// Get aggregate statistics (no auth needed)
const stats = await lcore.getAggregates('lending', 'balance');
console.log('Distribution:', stats.counts);

Error Handling​

Error Response Format​

interface ErrorResponse {
error: string;
code: string;
details?: Record<string, unknown>;
}

Common Errors​

CodeHTTP StatusDescriptionSolution
UNAUTHORIZED401Invalid or missing API keyCheck your API key
FORBIDDEN403No grant for this attestationRequest user grant
GRANT_EXPIRED403Grant has expiredRequest new grant
GRANT_REVOKED403User revoked the grantRequest new grant
NOT_FOUND404Attestation doesn't existVerify attestation ID
RATE_LIMITED429Too many requestsImplement backoff
LCORE_UNAVAILABLE503L{CORE} service is downRetry with backoff

Error Handling Example​

async function safeGetAttestation(attestationId: string) {
try {
return await lcore.getAttestation(attestationId);
} catch (error) {
if (error.code === 'GRANT_EXPIRED') {
// Prompt user to re-grant
await promptForNewGrant(attestationId);
return await lcore.getAttestation(attestationId);
}

if (error.code === 'RATE_LIMITED') {
// Wait and retry
await sleep(error.details?.retryAfter || 60000);
return safeGetAttestation(attestationId);
}

throw error;
}
}

Best Practices​

1. Minimize Data Access​

Request only the data you need:

// Bad: Request full access when you only need balance
const grant = await requestGrant({
grantType: 'full', // Requests everything
// ...
});

// Good: Request partial access with specific keys
const grant = await requestGrant({
grantType: 'partial',
dataKeys: ['balance'], // Only what you need
// ...
});

2. Handle Grant Expiration​

async function ensureValidGrant(attestationId: string): Promise<Grant> {
const grants = await lcore.getMyGrants();
const grant = grants.find(g => g.attestationId === attestationId);

if (!grant) {
throw new Error('No grant exists for this attestation');
}

if (new Date(grant.expiresAt) < new Date()) {
throw new Error('Grant has expired');
}

// Warn if expiring soon
const daysUntilExpiry = (new Date(grant.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry < 7) {
console.warn(`Grant expires in ${Math.floor(daysUntilExpiry)} days`);
}

return grant;
}

3. Cache Appropriately​

// Cache aggregate results (they change slowly)
const aggregateCache = new Map<string, { data: any; expires: number }>();

async function getCachedAggregates(domain: string, bucketKey: string) {
const cacheKey = `${domain}:${bucketKey}`;
const cached = aggregateCache.get(cacheKey);

if (cached && cached.expires > Date.now()) {
return cached.data;
}

const data = await lcore.getAggregates(domain, bucketKey);
aggregateCache.set(cacheKey, {
data,
expires: Date.now() + 5 * 60 * 1000, // 5 minute cache
});

return data;
}

// Don't cache individual attestation data - it may change

4. Respect User Privacy​

// Don't log sensitive data
function logAttestation(attestation: AttestationData) {
console.log('Attestation:', {
id: attestation.id,
provider: attestation.provider,
// Don't log: ownerAddress, buckets, data
});
}

// Don't store more than you need
async function processLoanApplication(attestationId: string) {
const attestation = await lcore.getAttestation(attestationId);

// Extract only what you need
const balanceBucket = attestation.buckets.balance;

// Don't store the full attestation
await saveLoanDecision({
qualifies: balanceBucket !== '<1k',
processedAt: new Date(),
// attestation: attestation, // Don't do this
});
}

5. Implement Retry Logic​

async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
backoffMs = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
if (error.code === 'RATE_LIMITED' || error.code === 'LCORE_UNAVAILABLE') {
await sleep(backoffMs * Math.pow(2, attempt));
continue;
}
throw error;
}
}
throw new Error('Unreachable');
}

// Usage
const attestation = await withRetry(() => lcore.getAttestation(id));

Example Implementations​

Lending Platform Integration​

// Full example: Lending platform checking user creditworthiness

import { LCoreClient, GrantType } from '@citychain/lcore-sdk';

const lcore = new LCoreClient({
attestorUrl: process.env.ATTESTOR_URL,
apiKey: process.env.LCORE_API_KEY,
});

interface LoanEligibility {
eligible: boolean;
maxAmount: number;
reason?: string;
}

async function checkLoanEligibility(
userAddress: string,
attestationId: string
): Promise<LoanEligibility> {
try {
// Get the attestation (requires valid grant)
const attestation = await lcore.getAttestation(attestationId);

// Check provider is trusted
const trustedProviders = ['chase', 'wellsfargo', 'bankofamerica'];
if (!trustedProviders.includes(attestation.provider)) {
return {
eligible: false,
maxAmount: 0,
reason: 'Provider not accepted',
};
}

// Check attestation freshness
if (attestation.freshnessScore < 0.5) {
return {
eligible: false,
maxAmount: 0,
reason: 'Attestation too old - please refresh',
};
}

// Determine loan amount based on balance bucket
const balanceBucket = attestation.buckets.balance;
const loanLimits: Record<string, number> = {
'<1k': 0,
'1k-5k': 1000,
'5k-10k': 2500,
'10k-25k': 5000,
'25k-50k': 10000,
'50k-100k': 25000,
'100k-500k': 50000,
'>500k': 100000,
};

const maxAmount = loanLimits[balanceBucket] || 0;

return {
eligible: maxAmount > 0,
maxAmount,
};
} catch (error) {
if (error.code === 'FORBIDDEN') {
return {
eligible: false,
maxAmount: 0,
reason: 'No access to attestation - please grant access',
};
}
throw error;
}
}

Dashboard with Aggregates​

// Example: Display aggregate statistics on a dashboard

import { useEffect, useState } from 'react';

interface DashboardStats {
totalUsers: number;
balanceDistribution: Array<{ bucket: string; count: number }>;
providerBreakdown: Array<{ provider: string; count: number }>;
}

function useLCoreStats(domain: string): DashboardStats | null {
const [stats, setStats] = useState<DashboardStats | null>(null);

useEffect(() => {
async function fetchStats() {
// These don't require authentication
const [balanceResponse, providerResponse] = await Promise.all([
fetch(`${ATTESTOR_URL}/api/lcore/aggregates/count-by-bucket?domain=${domain}&bucket_key=balance`),
fetch(`${ATTESTOR_URL}/api/lcore/aggregates/count-by-provider?domain=${domain}`),
]);

const balance = await balanceResponse.json();
const providers = await providerResponse.json();

setStats({
totalUsers: balance.total,
balanceDistribution: balance.counts.map((c: any) => ({
bucket: c.bucketValue,
count: c.count,
})),
providerBreakdown: providers.counts.map((c: any) => ({
provider: c.provider,
count: c.count,
})),
});
}

fetchStats();
// Refresh every 5 minutes
const interval = setInterval(fetchStats, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [domain]);

return stats;
}

// Usage in component
function Dashboard() {
const stats = useLCoreStats('lending');

if (!stats) return <Loading />;

return (
<div>
<h1>Lending Pool Statistics</h1>
<p>Total verified users: {stats.totalUsers}</p>

<h2>Balance Distribution</h2>
<BarChart data={stats.balanceDistribution} />

<h2>Providers</h2>
<PieChart data={stats.providerBreakdown} />
</div>
);
}

Document History​

VersionDateChanges
1.12025-01-12Added decryption proof verification section
1.02025-01-12Initial dApp integration guide