Inspiration

Most applications that need to verify something about a person follow a simple pattern: ask for the person's data, send it to a server, check it, and return a decision.

That works, but it creates an uncomfortable trade-off. A verifier may only need to know whether a requirement is satisfied, but the applicant is often forced to reveal the underlying information.

For example, imagine a lender saying:

"The applicant must be at least 21 years old and earn at least $4,000 per month."

A traditional system might require the applicant to submit their age and income. But the verifier does not actually need to know that the applicant is 23 and earns $5,000. They only need to know whether the applicant satisfies the conditions.

That led us to the idea behind VeilAI:

Can we let someone prove that they satisfy a requirement without revealing the private information used to prove it?

We wanted to combine a natural-language interface with zero-knowledge verification so that the verifier could describe a requirement normally, while the applicant could prove eligibility without disclosing the underlying values.

This also gave us a practical reason to explore Midnight. Instead of treating privacy as something added on top of a blockchain application, we wanted privacy-preserving computation to be part of the core verification flow.


What it does

VeilAI is a privacy-preserving eligibility verification system.

A verifier can enter a requirement such as:

"Applicant must be at least 21 and make at least $4,000 per month."

The AI interprets the natural-language requirement and converts it into a structured policy:

{
  "version": 1,
  "logic": "AND",
  "rules": [
    {
      "field": "age",
      "operator": ">=",
      "value": 21
    },
    {
      "field": "income",
      "operator": ">=",
      "value": 4000,
      "unit": "USD_MONTHLY"
    }
  ]
}

That policy is then passed into the privacy-preserving verification layer.

The applicant's private credential contains information such as:

Age: 23
Monthly income: $5,000

Those values remain on the applicant side and are supplied to the Midnight computation through private state and witnesses.

The Compact contract verifies conditions such as:

$$ age \geq minAge $$

and

$$ income \geq minIncome $$

without requiring the verifier to receive the underlying values.

The final result is therefore about the claim, rather than the private data:

Qualified ✅

instead of:

Age = 23
Income = $5,000

The central idea is simple:

Reveal the answer, not the information used to calculate the answer.

For the Prototype, we deliberately kept the scope narrow and focused on age and monthly income so that we could demonstrate the privacy-preserving verification mechanism clearly instead of trying to solve every possible identity or credential problem at once.


How we built it

We split VeilAI into three major layers: the verifier-facing frontend, the AI/backend policy layer, and the Midnight privacy layer.

1. Natural-language policy generation

The first part of the system is an Express/TypeScript backend.

The backend exposes:

POST /api/policy/parse

with input such as:

{
  "requirement": "Applicant must be 21+ and make at least $4K per month."
}

The backend sends the requirement to an LLM and uses structured output rather than trusting arbitrary model-generated text.

We deliberately treated AI output as untrusted input:

LLM output
    ↓
JSON parsing
    ↓
Zod validation
    ↓
Semantic validation
    ↓
Normalized EligibilityPolicy

This was important because an LLM should not be responsible for deciding whether an applicant qualifies. Its job is only to understand the verifier's language and translate it into a deterministic policy representation.

The backend therefore became a policy-generation layer rather than an eligibility engine.


2. Privacy-preserving verification

The second layer is the Midnight/Compact side.

We created private applicant state containing values such as:

age
income

and connected that state to Compact through witnesses.

The witness layer provides private values to the circuit without turning them into normal public application data.

Our Compact verification logic is intentionally simple for the Prototype:

private applicant age
private applicant income

public minimum age
public minimum income

        ↓

age >= minimum age
AND
income >= minimum income

        ↓

proof succeeds or fails

The important distinction is that the applicant's values are used inside the privacy-preserving computation rather than being sent to the verifier or exposed as part of the application response.

We also intentionally avoided implementing our own cryptography. The difficult cryptographic machinery is handled by Midnight's infrastructure; our engineering work focused on connecting our application logic to that infrastructure correctly.


3. Connecting AI policy output to Midnight

One of the biggest integration gaps we encountered was that the two halves of the project initially spoke different languages.

The AI/backend produced:

EligibilityPolicy

while the Midnight verification function expected:

{
  minAge: number;
  minIncome: number;
}

Both pieces worked independently, but they were not actually connected.

We therefore introduced the concept of a policy adapter:

EligibilityPolicy
        ↓
policyAdapter
        ↓
{
  minAge,
  minIncome
}
        ↓
executeVerification()
        ↓
Midnight

For the Prototype, we restricted this adapter to policies that the current Compact contract could genuinely support:

age >= X
AND
income >= Y

This was an important design decision. Rather than allowing the AI to generate arbitrary policy logic that the Midnight contract could not execute, we constrained the supported policy space to what we could actually verify end-to-end.


4. Frontend integration

The frontend acts as the user-facing layer for both sides of the system.

The verifier provides a natural-language requirement.

The frontend sends that requirement to the backend and receives a structured policy.

The interpreted policy can then be displayed before verification, making the AI's interpretation visible instead of silently trusting it.

The Midnight-side logic then consumes the policy and produces a verification result such as:

{
  "qualified": true,
  "checks": [
    {
      "field": "age",
      "operator": ">=",
      "required": 21
    },
    {
      "field": "income",
      "operator": ">=",
      "required": 4000,
      "unit": "USD_MONTHLY"
    }
  ],
  "proofVerified": true
}

The frontend therefore gets enough information to communicate the result without needing to display the applicant's private values.


Challenges we ran into

Understanding the actual privacy boundary

One of our first challenges was realizing that "using zero-knowledge" is not enough by itself.

We had to carefully decide where private information was allowed to exist.

It would have been very easy to build a conventional backend endpoint such as:

{
  "age": 23,
  "income": 5000
}

and simply check those values on a server.

That would have made the application work, but it would completely undermine the purpose of the project.

We therefore had to distinguish between:

  • information needed to define a requirement
  • information needed to prove the requirement
  • information the verifier is actually allowed to see

That led us to keep the verifier's policy in the public application flow while keeping the applicant's actual values on the private Midnight side.


Separating "understanding" from "verification"

Another challenge was defining what the AI should actually do.

Initially, it is tempting to let the model interpret a sentence and directly decide whether someone qualifies.

We deliberately rejected that architecture.

Instead:

AI:
"What does the verifier want?"

Midnight:
"Does the applicant actually satisfy it?"

This separation made the system much easier to reason about.

The AI produces a deterministic policy.

The cryptographic verification layer produces the actual eligibility result.

This also made the system safer because a hallucination or unexpected AI response cannot directly become a qualification decision without passing through schema and semantic validation.


Compact/runtime integration

The Midnight portion introduced a completely different development model from normal web development.

We had to understand:

  • Compact circuits
  • private state
  • witnesses
  • generated managed contract code
  • proof generation
  • contract invocation
  • public and private inputs
  • Midnight client APIs
  • indexer/RPC configuration

A particularly tricky aspect was understanding that a Compact assertion such as:

assert age >= minAge;

does not behave like a normal JavaScript if statement.

If an assertion fails, the circuit execution aborts and no successful proof is produced.

That meant our frontend result model could not simply assume that the contract would return detailed information such as:

age passed
income failed

Our current contract fundamentally gives us a higher-level outcome:

verification succeeded

or:

verification failed

We had to design the API around what the cryptographic layer actually proves instead of inventing information that the circuit does not provide.


Generated contract code versus source code

Another challenge was dealing with the distinction between our Compact source and generated managed contract code.

The TypeScript side imports generated contract artifacts, while the actual logic originates in the Compact source.

This created an important engineering rule for us:

Never treat generated contract code as the source of truth.

When debugging integration issues, we had to trace:

Compact source
        ↓
generated contract
        ↓
TypeScript imports
        ↓
runtime invocation

That was significantly more complicated than connecting two normal TypeScript modules.


Proof provider/runtime configuration

Local development introduced another practical challenge.

Our Midnight integration initially referenced a local proof-provider endpoint such as:

127.0.0.1:6300

That works only when the expected proof infrastructure is actually running locally.

It also created an important deployment realization:

127.0.0.1 means the machine running the code.

A proof provider configured to work on a developer machine cannot simply be deployed unchanged and expected to work for every user.

This forced us to think about development and deployment environments separately and made runtime configuration part of the integration problem rather than an afterthought.


Integrating independently developed branches

The project was also developed across multiple branches and by different people, which created a very normal but painful software-engineering problem: the individual parts were more mature than the integration.

We had frontend code, backend/AI code, and Midnight code that were each useful independently but did not initially share a single consistent architecture.

The challenge became less about writing new code and more about:

  • identifying the source of truth
  • resolving duplicate abstractions
  • agreeing on interfaces
  • mapping data structures
  • preserving privacy guarantees
  • avoiding duplicate implementations
  • deciding which logic belonged in which runtime

The project improved significantly once we stopped thinking of the branches as separate features and started treating them as a single system with explicit interfaces.


Designing a useful result without leaking private data

The frontend wanted a structured result such as:

{
  "qualified": true,
  "checks": [...],
  "proofVerified": true
}

The challenge was deciding what checks should contain.

It is tempting to return:

Age: 23 ✅
Income: $5,000 ✅

but that directly leaks the information we're trying to protect.

We therefore separated:

what was required

from:

what the applicant's actual values were

and let the cryptographic verification result determine the final qualification status.

That small API design decision was actually an important part of preserving the project's core privacy model.


Accomplishments that we're proud of

The biggest accomplishment is that we did not stop at a conceptual privacy demo.

We built the individual pieces required for a real flow:

Natural language requirement
        ↓
AI policy parsing
        ↓
Structured EligibilityPolicy
        ↓
Policy validation
        ↓
Policy → Midnight adapter
        ↓
Private applicant state
        ↓
Compact circuit
        ↓
ZK verification
        ↓
Frontend-friendly result

We are particularly proud of the fact that the backend deliberately does not require applicant age or income in order to generate the policy.

The application architecture itself reflects the privacy goal.

We are also proud of the separation of responsibilities:

  • AI interprets language.
  • Backend validates the policy.
  • Midnight verifies the claim.
  • The applicant retains their private information.
  • The frontend communicates the result.

Another accomplishment was keeping the Prototype focused. Rather than trying to solve document authenticity, OCR, employer integrations, identity verification, and every possible policy type simultaneously, we concentrated on one clear privacy primitive: proving age and income thresholds without revealing age and income.

That gave us a much stronger demonstration of the technology.


What we learned

Zero-knowledge is fundamentally an application architecture problem

Before building VeilAI, it is easy to think of zero-knowledge proofs as simply a cryptographic feature.

In practice, the harder part was deciding:

What information stays private? What information becomes public? Where does computation happen? Which component is trusted to do what?

The privacy guarantee depends on the architecture around the proof, not just the proof itself.


AI and cryptography should have different responsibilities

We learned that AI is useful for translating ambiguous human language into structured intent, but it should not be the final authority for security-sensitive decisions.

Our architecture became much clearer once we made this separation:

$$ \text{Natural Language} \xrightarrow{\text{AI}} \text{Policy} \xrightarrow{\text{ZK Verification}} \text{Result} $$

AI handles interpretation.

Cryptographic verification handles proof.


Integration is often harder than implementation

Each subsystem can be "working" while the application as a whole is still broken.

We learned that the real engineering work often happens at the boundaries:

API types
Runtime assumptions
Generated code
Environment variables
Client/server boundaries
Private/public state
Error semantics

This project taught us to define interfaces early rather than waiting until the end to discover that two correct components expect different inputs.


Blockchain development has a different debugging mindset

With a normal backend, you can inspect values freely.

With a privacy-preserving system, doing that can defeat the architecture.

We had to learn to debug without casually printing private state and to reason about failures at the proof/circuit level rather than simply dumping all inputs into a console.


Scope discipline matters

One of our biggest lessons was that there are multiple separate problems hiding behind a seemingly simple sentence like "verify this person's income."

There is:

  1. extracting information from a document,
  2. deciding whether the source is trustworthy,
  3. issuing a credential,
  4. keeping the credential private,
  5. proving a claim about it,
  6. interpreting a verifier's requirement.

Trying to solve all six problems would have made the Prototype extremely difficult.

We deliberately focused on the privacy-preserving proof problem and treated the credential as an already-trusted input for the Prototype.

That allowed us to demonstrate the core idea instead of getting lost in peripheral systems.


What's next for VeilAI

The next step is to make the current privacy-preserving verification model more expressive and production-ready.

Our immediate direction is to strengthen the policy layer and Midnight integration so that the same structured policies generated by the AI can map cleanly into a richer set of privacy-preserving verification circuits.

Beyond that, there are several natural extensions.

More kinds of claims

Instead of only:

age >= X
income >= Y

we could support claims involving:

  • authenticity verification
  • residency
  • employment status
  • education
  • credit-related attributes
  • account balances
  • professional credentials
  • other eligibility conditions

without requiring the verifier to see the underlying personal information.

Richer policy logic

The current Prototype intentionally uses:

age >= X AND income >= Y

A future version could support composable policies involving more sophisticated combinations of conditions while keeping the same privacy boundary.

Real verifiable credentials

Our current demo assumes that the private credential is trusted.

A future system could integrate real credential issuers such as employers, universities, financial institutions, or identity providers.

That would let VeilAI move from:

"Assume these private claims are trusted"

to:

"Use a cryptographically issued credential and prove only the required properties of it."

Better verifier experience

We also want to make the policy interpretation more transparent and controllable.

Instead of the verifier simply trusting an AI interpretation, the UI can show exactly what the system understood and allow the verifier to confirm the policy before a proof is generated.

Production deployment

Finally, we want to move from a development environment into a fully deployable architecture with production-safe environment configuration, reliable Midnight proof infrastructure, and a smoother wallet/client experience.

The long-term vision for VeilAI is not to hide information for the sake of hiding it.

It is to change the default interaction from:

"Give me your private data so I can check it."

to:

"Prove to me that the condition is satisfied. You don't need to show me the underlying data."

That is the principle we want VeilAI to demonstrate.

Built With

Share this project:

Updates

Submission history