Skip to main content
Nullis ships two example apps that share a single contract and a single circuit. They aren’t toys glued on top — they’re the proof of the hardest claim in the system: the same credential authorizes actions in both apps, yet is unlinkable between them.

Two apps, one engine

Remittance

examples/remittance/ — a corridor policy (e.g. NG → UK) with an amount cap. A user proves corridor eligibility privately; the contract executes the exact transfer, once.

RWA access

examples/rwa-access/ — an access-grant policy for a tokenized real-world asset. A user proves they’re on the approved list; the contract grants access, once.
RemittanceRWA access
Policy id777888
action_typetransferaccess_grant
app_domain4424048879
What’s provencorridor eligibilityapproved-list membership
What executesasset transferaccess grant
Nullifier0x09e0…436ba08c0x0532…357f2911
Same engine, different policies, different app_domain — and therefore different, unlinkable nullifiers.

Walkthrough — the remittance app

1

The app publishes a policy

{
  "policy_id": 777,
  "version": 1,
  "action_type": "transfer",
  "asset": "USDC",
  "max_amount": 1000,
  "approved_root": "0x…",       // corridor-eligible commitments
  "app_domain": "remittance-ng-uk",
  "expiry": 1785000000
}
Hashed to policy_hash and registered on-chain (PolicyPublished).
2

A user builds a request + proof

The user holds a credential_secret whose commitment is in the corridor’s approved root. buildRequest derives the canonical public inputs; Noir + Barretenberg generate the proof.
const req = buildRequest({
  policyId: 777n, policyHash, policyVersion: 1, approvedRoot,
  appDomainHash, policyExpiry, networkId, nullisContract,
  credentialSecret,
  action: { actionType: 1, recipient, amount: 100n, asset, consumingContract, intentNonce },
});
3

verify_and_execute — one call

const receipt = await nullis.verifyAndExecute({
  policyId: 777n, publicInputs: req.publicInputs, proof, action: req.action,
});
// → result: VERIFIED, executed: true — 100 USDC moved, identity never revealed
The RWA access app is the same shape with action_type: "access_grant" and a different approved root — proving membership in an asset’s allowlist instead of a corridor.

Run the unlinkability proof

The load-bearing example. One credential, two apps, two unlinkable nullifiers:
npm run build -w @nullis/core -w @nullis/sdk -w @nullis/issuer
node examples/unlinkability.mjs
Sample output (captured in artifacts/demo-results.json):
{
  "credentialCommitment": "0x17b1b911…e0c4ab09",
  "apps": {
    "remittance": { "policyId": "777", "appDomainHash": "44240", "nullifier": "0x09e0…436ba08c" },
    "rwaAccess":  { "policyId": "888", "appDomainHash": "48879", "nullifier": "0x0532…357f2911" }
  },
  "unlinkable": true
}
The same credentialCommitment produces two different nullifiers. There is no shared value an observer could use to link the remittance user to the RWA-access user — at the proof/nullifier layer. See Unlinkability for the scope of that claim.

The evidence artifacts

The repo carries a full, reproducible evidence package:
ArtifactWhat it holds
README.md · EVIDENCE.mdThe reviewer-grade walkthrough and the real-vs-mocked line
SECURITY.md · BENCHMARKS.mdThreat model and measured numbers
examples/remittance/ · examples/rwa-access/The two apps above
examples/unlinkability.mjsThe cross-app unlinkability proof
artifacts/testnet-transactions.jsonEvery live testnet transaction
artifacts/demo-results.json · submission-evidence.jsonDemo results and the submission manifest

See the live evidence

Every claim as a real testnet transaction.

The SDK

The one-line verifyAndExecute these apps call.