Skip to main content

Nillion-Ethereum Integration: MetaMask + EIP-712 DID Generation

· 6 min read
Nillion Team
Nillion Development Team

Understanding nilDB Encryption

Technical documentation for Ethereum wallet integration with Nillion using EIP-712 signatures and DIDs.

Overview

Nillion integrates with Ethereum wallets via MetaMask to create passwordless authentication. Ethereum addresses are transformed into did:ethr: format DIDs through EIP-712 typed data signing.

Key Components:

  • DID Format: did:ethr:<ethereum_address>
  • Signing Standard: EIP-712 typed data
  • SDK: @nillion/nuc handles DID generation
  • Client Library: viem for Ethereum interactions
  • Storage: React state (runtime), localStorage (tokens), NilDB (profiles)

Authentication Flow

MetaMask Connection → DID Generation → Subscription Check 
→ Token Generation → Profile Registration → Storage

Files:

  • src/context/NillionContext.tsx - Wallet/signing logic
  • src/hooks/useInitializeSessionMutation.ts - New session flow
  • src/hooks/useLoginMutation.ts - Existing session restoration
  • src/context/AuthFlowManager.tsx - Flow orchestration

1. DID Generation

DIDs are generated by the Nillion SDK, not manually constructed.

// Create signer from Web3 wallet
const nucSigner = Signer.fromWeb3({
getAddress: async () => account, // MetaMask address
signTypedData: async (domain, types, message) => {
return walletClient.signTypedData({...});
},
});

// Generate DID
const did = await nucSigner.getDid();
// Result: { didString: "did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }

Location: src/context/NillionContext.tsx (lines 72-101)


2. MetaMask Signing

EIP-712 signatures occur in the signTypedData callback passed to Signer.fromWeb3().

signTypedData: async (domain, types, message) => {
const primaryType = Object.keys(types).find(k => k !== "EIP712Domain");

// Handle chain switching if needed
const domainChainId = Number(domain?.chainId);
if (domainChainId !== activeChainId) {
await metaMaskProvider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${domainChainId.toString(16)}` }],
});
}

// Sign via viem
return walletClient.signTypedData({
account,
domain,
types,
primaryType,
message,
});
}

Signing Triggers:

  • Root token generation (refreshRootToken())
  • Invocation token creation (signAndSerialize(signer))
  • Builder profile registration

Location: src/context/NillionContext.tsx (lines 74-98)


3. EIP-712 Payload Structure

The Nillion SDK (@nillion/nuc) determines the exact EIP-712 structure. The application receives and passes this payload to MetaMask.

Inferred Structure (based on standard usage):

// Root Token Generation
{
domain: {
name: "Nillion",
version: "1",
chainId: 1 | 11155111 // Mainnet or Sepolia
},
types: {
EIP712Domain: [...],
Token: [
{ name: "subscriber", type: "address" },
{ name: "service", type: "string" },
{ name: "nonce", type: "uint256" }
]
},
message: {
subscriber: "0x742d35Cc...",
service: "nildb",
nonce: 123
}
}

// Invocation Token
await Builder.invocationFrom(rootToken)
.audience(node.id)
.command(NucCmd.nil.db.root)
.signAndSerialize(signer); // Triggers EIP-712 signing

Supported Chains:

  • Mainnet (chainId: 1)
  • Sepolia (chainId: 11155111)

4. Token Architecture

Token Hierarchy

Root Token (Subscriber Identity)
└─ Invocation Tokens (Per-Operation)
├─ Node-specific (audience: did:key:node_id)
└─ Command-specific (NucCmd.nil.db.*)

Root Token

  • Purpose: Proves subscription to Nillion
  • Generation: Nilauth service validates signature, returns token
  • Format: Base64URL-encoded JWT
  • Storage: localStorage as nillion_rootToken

Invocation Tokens

  • Purpose: Authorize specific operations on specific nodes
  • Generation: Client-side via Builder.invocationFrom(rootToken)
  • Audience: Individual node DIDs (did:key:z6Mk...)
  • Storage: localStorage as nillion_nildbTokens (JSON object)
// Generate invocation token
const invocation = await Builder.invocationFrom(rootToken)
.audience(node.id) // Target node
.command(NucCmd.nil.db.root) // Authorized operation
.signAndSerialize(signer); // EIP-712 sign

Location: src/hooks/useInitializeSessionMutation.ts (lines 39-57)


5. Storage & Persistence

Client-Side Storage

React State (Runtime):

{
signer: NucSigner,
did: "did:ethr:0x...",
wallets: {
isMetaMaskConnected: true,
metaMaskAddress: "0x..."
}
}

localStorage (Persistent):

localStorage.setItem("nillion_rootToken", "eyJhbGci...");
localStorage.setItem("nillion_nildbTokens", JSON.stringify({
"did:key:node1": "eyJhbGci...",
"did:key:node2": "eyJhbGci...",
"did:key:node3": "eyJhbGci..."
}));

NilDB Storage

Builder Profiles:

{
did: "did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
name: "Demo Builder"
// Encrypted with Blindfold, replicated across 3 nodes
}

Location: src/hooks/usePersistedConnection.ts, src/hooks/useInitializeSessionMutation.ts


6. Network Architecture

Endpoints

// Nilauth (Authentication Service)
https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz

// NilDB Nodes (Data Storage)
https://nildb-stg-n1.nillion.network
https://nildb-stg-n2.nillion.network
https://nildb-stg-n3.nillion.network

// Nilchain RPC (Internal to SDK)
http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz

Client Architecture

Browser
├─ MetaMask (EIP-712 signing)
├─ Nillion SDK (DID generation, tokens)
├─ React App (UI, state management)
└─ HTTP Requests → Nillion Network
├─ Nilauth (subscription check, root token)
└─ NilDB Nodes (profile read/write)

Location: src/config.ts


7. Verification

All verification happens server-side on the Nillion Network.

Nilauth Service:

  • Validates EIP-712 signatures on root tokens
  • Checks subscription status
  • Returns signed root tokens

NilDB Nodes:

  • Verify invocation token signatures
  • Validate token audience (matches node DID)
  • Authorize commands (NucCmd permissions)

What Gets Verified:

  • EIP-712 signature validity
  • DID format (did:ethr:0x...)
  • Subscription status
  • Token authenticity
  • Token audience/commands

8. Complete Flow Examples

New Session (First-Time Login)

// 1. Connect MetaMask
const account = await metaMaskProvider.request({
method: "eth_requestAccounts"
});

// 2. Generate DID
const signer = Signer.fromWeb3({...});
const did = await signer.getDid();
// Result: "did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"

// 3. Check subscription
const subscription = await nillionClient.fetchNodeSubscriberInfo(did.didString);

// 4. Generate root token (triggers EIP-712 signature)
const rootToken = await nillionClient.refreshRootToken({
signer,
service: "nildb"
});

// 5. Generate invocation tokens (one per node)
const invocations = await Promise.all(nodes.map(node =>
Builder.invocationFrom(rootToken)
.audience(node.id)
.command(NucCmd.nil.db.root)
.signAndSerialize(signer)
));

// 6. Register builder profile
await nillionClient.createProfile({
auth: { invocations },
blindfold: { operation: "store" }
});

// 7. Store tokens
localStorage.setItem("nillion_rootToken", rootToken);
localStorage.setItem("nillion_nildbTokens", JSON.stringify(invocations));

Existing Session (Returning User)

// 1. Connect MetaMask
const account = await metaMaskProvider.request({
method: "eth_requestAccounts"
});

// 2. Restore DID from signer
const signer = Signer.fromWeb3({...});
const did = await signer.getDid();

// 3. Load tokens from localStorage
const rootToken = localStorage.getItem("nillion_rootToken");
const nildbTokens = JSON.parse(localStorage.getItem("nillion_nildbTokens"));

// 4. Validate by reading profile
const profile = await nillionClient.readProfile({
auth: { invocations: nildbTokens }
});

// 5. Restore state
setState({ signer, did: did.didString, wallets: {...} });

Location:

  • New: src/hooks/useInitializeSessionMutation.ts
  • Existing: src/hooks/useLoginMutation.ts

9. DID Usage Patterns

As Identity

// Subscription lookup
const info = await nillionClient.fetchNodeSubscriberInfo(
"did:ethr:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
);

// Profile operations
await nillionClient.createProfile({ /* DID embedded in auth */ });
const profile = await nillionClient.readProfile({ /* ... */ });

As Namespace

// Storage key
{
did: "did:ethr:0x742d35Cc...", // Primary key
name: "Demo Builder",
// ... other fields
}

As Cryptographic Proof

// Token claims
{
subscriber: "did:ethr:0x742d35Cc...",
// Signature proves control of Ethereum address
}

10. Error Handling

Chain Mismatch

// Automatically switch chains
if (domainChainId !== activeChainId) {
await metaMaskProvider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hex }],
});
}
// Fallback: attempt signature anyway

Duplicate Registration

// NilDB returns 400 on duplicate profile
if (error.code === 400 && error.message.includes("DuplicateEntry")) {
// Gracefully continue - profile already exists
}

Token Expiration

// Refresh root token
const newRootToken = await nillionClient.refreshRootToken({
signer,
service: "nildb"
});

Technical Requirements

Browser Requirements:

  • MetaMask installed
  • Active Nillion subscription
  • Supported Ethereum chain (Mainnet or Sepolia)

Network Access:

  • CORS-enabled endpoints
  • No direct RPC interaction (handled by MetaMask/SDK)

Security:

  • All signatures occur in MetaMask (user approval required)
  • Tokens stored client-side (localStorage)
  • Profiles encrypted with Blindfold in NilDB

Key Takeaways

  1. SDK-Driven: Nillion SDK handles DID generation, payload construction, and cryptographic operations
  2. Client-Side Flow: Entire authentication process runs in browser
  3. Server-Side Verification: Signature validation occurs on Nillion Network backend
  4. Token Hierarchy: Root token → invocation tokens → authorized operations
  5. EIP-712 Standard: Ethereum-native signing for wallet integration
  6. Storage Strategy: Runtime (React state), persistent (localStorage), distributed (NilDB)

Codebase: https://github.com/geniusyinka/nillion-metamask-login
Network: Nillion Testnet
Chains: Ethereum Mainnet (1), Sepolia (11155111)

Understanding nilDB Encryption

· 4 min read
Nillion Team
Nillion Development Team

Understanding nilDB Encryption

When building privacy-preserving applications, a common challenge is figuring out how to keep sensitive user data safe while still being able to work with it. Traditional databases store all information in one place making them a single point of failure if breached.

nilDB takes a very different approach. It’s part of Nillion’s blind modules, designed for secure and decentralized data storage. Instead of storing plaintext data values, developers can use blindfold encryption within the SecretVaults SDK to split encrypted data into mathematical shares, then distribute and store them across multiple nilDB nodes. No single node ever has access to the full value. To reconstruct the data, you need multiple shares from different nodes.

This approach gives you a much stronger security guarantee: if one node is compromised, the attacker gets nothing useful.

From APIs to BPIs: Min-maxing the use of Sensitive Data

· 8 min read
Nillion Team
Nillion Development Team

From APIs to BPIs: Min-maxing the use of Sensitive Data

Today, most user data sits behind APIs. Whether its storage is delegated to an external entity (e.g., a social network), it's stored directly on a blockchain, or it remains on the user's device (e.g., browser history), third-party apps and services request access to that data via an API. But most often, users are not compensated directly for granting such access. This is the case even when the API itself may charge the third-party app for such access! Furthermore, once API access is granted, data is retrieved in its plaintext form. The extent to which that plaintext data is exposed is often at the discretion of the third party.

Tickr: Building Trust in Trading Performance

· 9 min read
Nillion Team
Nillion Development Team

Tickr: Building Trust in Trading Performance

At Nillion, we're best known for building advanced privacy-preserving infrastructure. However, there's another essential, though often overlooked, concept that we are enabling - trust. One could argue this is the root concept of all, what can I trust, and why can I trust it?

In this article, we explain what motivated us to build Tickr, and how we applied Nillion's technology, nilDB (secure database) and nilCC (confidential computing), to solve very specific, but distinct, problems for traders and their communities on centralised exchanges (CEXs) and decentralised exchanges (DEXs).

Private Data Services Deserve Private Subscriptions

· 4 min read
Nillion Team
Nillion Development Team

Private Data Services Deserve Private Subscriptions

At Nillion, we set ourselves the challenge of building the blind computer, a computational stack that makes it easy for developers to build privacy-preserving apps. But as we developed our Private Storage and Private LLMs services, we faced a fundamental contradiction: how do you provide access to privacy first services without compromising privacy from the very first interaction?

The irony was glaring. Here we were, building technology that could process your data without ever seeing it, yet to use our services, you potentially still need to hand over your email, credit card details, and billing address just like every other API provider. For a privacy project, this felt fundamentally wrong, so we built our own solution.

Nillion Network Architecture for a PETs Ecosystem

· 7 min read
Nillion Team
Nillion Development Team

Nillion Network Architecture for a PETs Ecosystem

Almost ten years ago, some of the current Nillion team members submitted a National Science Foundation research proposal that envisioned a decentralized secure storage and computation ecosystem in which node operators with various roles would enable, enhance, and monitor web-compatible apps and workflows over encrypted data. Today, we are finally seeing that vision come to life at Nillion as we build infrastructure and tools for creating competitive, usable apps that still protect user data. In this article, we discuss the philosophy and principles that motivate the architecture of the Nillion Network. We also present how the architecture's characteristics support a role-based PETs ecosystem that echoes and expands on the original vision.