v0.1.0-beta

Integration Guide

Integrate Veil into your DeFi protocol in five steps: register as an issuer, submit behavioral events as users interact with your protocol, obtain a challenge, request a credit decision using the user's Veil DID, and act on the result.

Prerequisites

  • Issuer registration: Your protocol must be approved by the Veil admin before you can submit events. Contact the Veil team to begin registration. You will receive an issuerPk — keep this secure.
  • Veil API access: Set VEIL_API_URL to the Veil backend URL provided during onboarding. Current preview endpoint: https://api.13-61-145-21.sslip.io/api/v1.
  • User Veil public key (userPk): Users derive their Veil public key in the dashboard. Behavioral events are keyed to this value. Events submitted before a user creates their score profile are queued until they do.
  • User Veil DID and Identity Pass: For credit decisions, the user must have minted a Veil Identity Pass on CKB. This generates their did:veil:… identifier and registers it on Midnight. The DID is the only piece of information you need to request a credit decision.
No blockchain access required on your end
Your protocol does not need a Midnight wallet or any blockchain SDK. The Veil backend handles all ZK proof generation, private state management, and transaction submission. You interact via standard REST over HTTPS.

Step 1: Register as an Issuer

Issuer registration is a one-time process handled by the Veil admin. The admin callsAdmin_addIssuer on-chain with your protocol name and contract address. Upon registration you receive:

  • issuerPk— your protocol's identity key within Veil. Include this in every event submission.

Registered issuers are listed in the contract's public ledger state. Any event submitted with an unregistered issuer key is rejected by the ZK circuit.

Issuer key security
Your issuerPkis a public key derived from your protocol's registration. While it is not a secret, it is your stable identity in the Veil system — store it securely in your backend configuration and do not rotate it without coordinating with the Veil admin.

Step 2: Submit Behavioral Events

As users interact with your protocol, submit behavioral events in real time. Four event types are available — see the Scoring Model for the full field reference.

User actionEvent endpoint
Loan repaid (on time or late)POST /scoring-events/repayments
Position liquidatedPOST /scoring-events/liquidations
User interacted with your protocolPOST /scoring-events/protocol-usage
Periodic debt state snapshotPOST /scoring-events/debt-states
typescriptSubmit a repayment event
const VEIL_API = process.env.VEIL_API_URL ?? 'https://api.13-61-145-21.sslip.io/api/v1';

// Submit a repayment event
const res = await fetch(`${VEIL_API}/scoring-events/repayments`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    userPk: '0xabc123...',        // user's Veil public key (32 bytes hex)
    issuerPk: '0xdef456...',      // your registered issuer public key
    paidOnTimeFlag: 1,            // 1 = on time, 0 = late
    amountWeight: 1000,           // normalized loan size
    eventEpoch: 3,                // current protocol epoch
  }),
});
const { job } = await res.json();
// → { id: "job_...", status: "pending" }

All event endpoints return a job. Because events are processed via ZK circuits on Midnight, they are queued asynchronously. You do not need to wait for one event to complete before submitting the next — events are processed independently.

Tip
Submit events only after the user has created a Veil score profile. The Midnight circuit rejects events for unknown users because there is no private score accumulator to update yet.

Step 3: Obtain a Challenge

Before requesting a credit decision, obtain a one-time challenge from the Veil backend. Challenges expire in 60 seconds and cannot be reused — they prevent replay attacks on the authorization flow.

typescriptPOST /challenges
// Request a one-time challenge (valid for 60 seconds)
const res = await fetch(`${VEIL_API}/challenges`, { method: 'POST' });
const { challenge, challengeExpiresAt } = await res.json();
// Use challenge in POST /credit-decisions within 60s

Present the challenge to the user so their CKB wallet can sign the decision authorization message (step 4).

Step 4: Request a Credit Decision

A credit decision verifies that the user holds a valid Veil Identity Pass on CKB, that their DID is registered on Midnight, and that they have authorized this specific request. The user signs a short message with their CKB wallet to prove they control their identity.

There are two ways to send a credit decision request — the DID flow is strongly recommended for all new integrations.

Using the DID Flow (Recommended)

The DID flow only requires the user's did:veil:…identifier. The backend resolves the DID internally to look up the user's identity pass, Midnight score, and CKB wallet — you do not need to pass userPk, veilIdHash, sporeId, or userCkbAddress separately.

typescriptPOST /credit-decisions (DID flow)
// The recommended flow uses the user's Veil DID (did:veil:0x...)
// No need to pass userPk, veilIdHash, sporeId, or userCkbAddress separately.

// 1. Get the user's Veil DID — they share it or you resolve it from their sporeId.
const veilDid = 'did:veil:0x...';                // e.g. from the user's dashboard

// 2. Obtain a fresh challenge
const { challenge } = await fetch(`${VEIL_API}/challenges`, { method: 'POST' })
  .then((r) => r.json());

// 3. Build the message the user's CKB wallet must sign
const verificationMethod = `${veilDid}#ckb-owner-1`;
const message = [
  'Veil credit decision authorization',
  `did:${veilDid}`,
  `challenge:${challenge}`,
  `verificationMethod:${verificationMethod}`,
  'registryVersion:1',
  'purpose:credit-decision',
].join('\n');

// 4. Have the user's CKB wallet sign the message (e.g. via CCC connector)
const authorization = await ckbSigner.signMessage(message);

// 5. Submit the credit decision request
const res = await fetch(`${VEIL_API}/credit-decisions`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    did: veilDid,                  // ← the Veil DID
    challenge,
    authorization: {
      ...authorization,
      verificationMethod,          // the verification method used
    },
  }),
});
const decision = await res.json();

The message the user's CKB wallet signs in the DID flow:

textDID authorization message
Veil credit decision authorization
did:<did:veil:0x...>
challenge:<challengeHex>
verificationMethod:<did:veil:0x...#ckb-owner-1>
registryVersion:1
purpose:credit-decision

Legacy Field-Based Flow

If the user has not yet registered their DID on Midnight (older accounts), you can fall back to passing the individual identity fields. This flow requires userPk, veilIdHash, sporeId, and userCkbAddress explicitly, and uses an older message format.

textLegacy authorization message
Veil credit decision authorization
challenge:<challengeHex>
userPk:<userPkHex>
veilIdHash:<veilIdHash>
sporeId:<sporeId>
DID registration happens automatically
When a user mints their identity pass in the current Veil dashboard, the backend automatically registers their DID on Midnight. All new users will have a registered DID — you only need the legacy flow for accounts created before the DID integration.
jsonCredit decision response
{
  "success": true,
  "approved": true,
  "scoreBand": "gold",
  "maxLtvBps": 6500,
  "riskPremiumBps": 150,
  "hasCreditScore": true,
  "reason": "Approved for gold tier",
  "veilIdHash": "0x...",
  "validAt": "2025-06-07T12:00:00.000Z"
}

The decision response contains:

  • approvedtrue if the user has a valid identity pass and meets the current decision policy
  • scoreBand — the trust tier: unranked · bronze · silver · gold · platinum
  • maxLtvBps and riskPremiumBps — lending policy outputs derived from the band (LTV in basis points, e.g. 6500 = 65%)
  • validAt — ISO 8601 timestamp of when the decision was issued
Decision freshness
Always issue a fresh challenge for each decision request. Challenges expire after 60 seconds and cannot be reused. Submitting a stale or already-used challenge returns a 401 error.

Step 5: Poll Job Status

Scoring events are queued as jobs. ZK proof generation on Midnight takes seconds to minutes depending on network load. Poll the job endpoint until the status is succeeded or failed.

StatusMeaningAction
pendingQueued or proof in progressContinue polling every 3–5 s
succeededEvent committed on-chainDone — move on
failedError during processingCheck error field; retry with backoff

TypeScript Example

A complete integration showing event submission, job polling, and credit decision using the DID flow:

typescriptveil-integration.ts
const VEIL_API = process.env.VEIL_API_URL ?? 'https://api.13-61-145-21.sslip.io/api/v1';

// Submit any scoring event
async function submitRepayment(params: {
  userPk: string;
  issuerPk: string;
  paidOnTimeFlag: 0 | 1;
  amountWeight: number;
  eventEpoch: number;
}): Promise<string> {
  const res = await fetch(`${VEIL_API}/scoring-events/repayments`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params),
  });
  if (!res.ok) throw new Error(`Submit failed: ${res.status}`);
  const { job } = await res.json();
  return job.id;
}

// Poll job until settled
async function waitForJob(jobId: string, timeoutMs = 60_000): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const { job } = await fetch(`${VEIL_API}/jobs/${jobId}`).then((r) => r.json());
    if (job.status === 'succeeded') return;
    if (job.status === 'failed') throw new Error(job.error ?? 'Job failed');
    await new Promise((r) => setTimeout(r, 3000));
  }
  throw new Error('Timed out waiting for job');
}

// Verify user trust using their Veil DID (recommended)
async function verifyWithDid(
  veilDid: string,
  ckbSigner: CkbSigner,  // your CCC-compatible signer
): Promise<{ approved: boolean; scoreBand: string }> {
  // 1. Obtain fresh challenge
  const { challenge } = await fetch(`${VEIL_API}/challenges`, { method: 'POST' }).then((r) => r.json());

  // 2. Build the DID authorization message
  const verificationMethod = `${veilDid}#ckb-owner-1`;
  const message = [
    'Veil credit decision authorization',
    `did:${veilDid}`,
    `challenge:${challenge}`,
    `verificationMethod:${verificationMethod}`,
    'registryVersion:1',
    'purpose:credit-decision',
  ].join('\n');

  // 3. Have the user sign with their CKB wallet
  const authorization = await ckbSigner.signMessage(message);

  // 4. Request the credit decision
  const res = await fetch(`${VEIL_API}/credit-decisions`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ did: veilDid, challenge, authorization: { ...authorization, verificationMethod } }),
  });
  if (!res.ok) throw new Error(`Decision failed: ${res.status}`);
  return res.json();
}

Policy Examples

Use the scoreBand from the credit decision to gate privileged actions in your protocol:

PolicyRecommended tier
Undercollateralized borrowingPlatinum only
Preferential interest rateGold or Platinum
Reduced liquidation bufferGold or Platinum
Basic credit accessBronze or above
Protocol access gatingSilver or above

Error Handling

StatusMeaningAction
400Missing or invalid request fieldsCheck request schema and required fields
401Invalid challenge, expired, or bad signatureRe-obtain a fresh challenge and re-sign
404Job, user, or DID not foundVerify the DID or userPk; user may not be registered
409DID found but not yet registered on MidnightUser should complete the dashboard flow; DID registry is pending
500Backend or Midnight node errorRetry with exponential backoff