Design
Contracts, pods, MATTR and the circuit
The documents every component was built against, rendered from the repo at build time.
Repo README
Layout and the one-paragraph pitch.
README.mdInterface contracts
Every endpoint, event and env var, plus amendments v1.1 to v1.7.
docs/CONTRACTS.mdCommBank as ADR
The kit data-in rebuild, event bridge, pod data access, revocation.
docs/adr-notes.mdData holders and Register
Kit topology, how a DH is generated, smoke test results.
docs/dh-notes.mdiOS app
Flows, debug panel, wallet SDK limits and proposed patches.
docs/ios-notes.mdIssuer
Offers, gateway, MATTR endpoints, listing thresholds.
docs/issuer-notes.mdCDR Data Pods
Runtime, programs, consent gating, retention and the ZK proof.
docs/pods.mdThe income circuit
Private inputs income and salt, public inputs threshold and commitment. Both amounts are range-checked to 32 bits, the commitment is recomputed with Poseidon, and GreaterEqThan(32) does the comparison.
packages/zk-circuits/circuits/income_gte.circomopen in source browserpragma circom 2.1.6;
// Proves: the prover knows (income, salt) such that
// commitment == Poseidon(income, salt) and income >= threshold
// without revealing income or salt. Amounts are whole AUD per year, 32-bit.
//
// Public signals (in this order): [threshold, commitment]
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/bitify.circom";
template IncomeGte(n) {
signal input income; // private
signal input salt; // private
signal input threshold; // public
signal input commitment; // public
// Range-check both amounts. GreaterEqThan is only sound for n-bit inputs.
component incomeBits = Num2Bits(n);
incomeBits.in <== income;
component thresholdBits = Num2Bits(n);
thresholdBits.in <== threshold;
// Binding: the commitment the pod signed must open to this income.
component h = Poseidon(2);
h.inputs[0] <== income;
h.inputs[1] <== salt;
commitment === h.out;
// income >= threshold
component gte = GreaterEqThan(n);
gte.in[0] <== income;
gte.in[1] <== threshold;
gte.out === 1;
}
component main {public [threshold, commitment]} = IncomeGte(32);
The MATTR credential configuration
The mDoc "CommBank ID – Income Verified", with the identity namespace plus the income namespace. income_annualised is net pay.
participants/cba-issuer/mattr/templates/credential-configurations/mdoc/commbank-id-income.yamlopen in source browser# "CommBank ID – Income Verified": the Commbank ID identity claims (financial-passport) plus the
# CDR-derived income. Issued only through the authorization-code flow, behind the offers gateway.
# identity <- cba-issuer claim source (data/customers.json; PF id_token carries only sub)
# income <- the same claim source, which merges the pod-runtime income claims (NET income)
branding:
backgroundColor: '#ffcc00'
name: CommBank ID – Income Verified
description: Identity plus income verified from your bank via the Consumer Data Right
claimMappings:
au.com.cba.commbank_id.1:
given_name:
display:
- locale: en-AU
name: Given name
mapFrom: claims.given_name
required: true
type: string
family_name:
display:
- locale: en-AU
name: Family name
mapFrom: claims.family_name
required: true
type: string
birth_date:
display:
- locale: en-AU
name: Date of birth
mapFrom: claims.birth_date
required: false
type: dateTime
email:
display:
- locale: en-AU
name: Email
mapFrom: claims.email
required: false
type: string
resident_country:
display:
- locale: en-AU
name: Country of residence
mapFrom: claims.resident_country
defaultValue: AU
required: false
type: string
kyc_level:
display:
- locale: en-AU
name: Identity assurance level
mapFrom: claims.kyc_level
required: false
type: string
customer_since:
display:
- locale: en-AU
name: Customer since
mapFrom: claims.customer_since
required: false
type: dateTime
verified_at:
display:
- locale: en-AU
name: Identity verified at
mapFrom: claims.verified_at
required: false
type: dateTime
age_over_18:
display:
- locale: en-AU
name: Over 18
mapFrom: claims.age_over_18
required: false
type: boolean
au.com.cba.income.1:
income_annualised:
display:
- locale: en-AU
name: Annual income (AUD)
mapFrom: claims.income_annualised
required: true
type: number
income_frequency:
display:
- locale: en-AU
name: Pay frequency
mapFrom: claims.income_frequency
required: true
type: string
income_employer:
display:
- locale: en-AU
name: Employer
mapFrom: claims.income_employer
required: false
type: string
income_verified_at:
display:
- locale: en-AU
name: Income verified at
mapFrom: claims.income_verified_at
required: true
type: dateTime
income_source:
display:
- locale: en-AU
name: Income source
mapFrom: claims.income_source
defaultValue: CDR
required: true
type: string
income_commitment:
display:
- locale: en-AU
name: Income commitment
mapFrom: claims.income_commitment
required: true
type: string
claimSourceId: ${CLAIM_SOURCE_ID_CBA_ISSUER}
expiresIn:
months: 3
includeStatus: true
type: au.com.cba.commbank-id-income
participants/cba-issuer/mattr/templates/claim-sources/cba-issuer-claims.yamlopen in source browser# Claim source for "CommBank ID – Income Verified". MATTR calls it at credential issuance (when
# the wallet retrieves the credential, not at offer time):
# GET ${OFFERS_API_URL}/claims/mattr?sub=<CBA sub>&purpose=credential-issuance
# x-api-key: <INTERNAL_API_KEY> (MATTR sends api-key auth as x-api-key; CONTRACTS v1.1 #6)
# The offers API answers with the customer's identity (data/customers.json - CBA PF's id_token
# carries only sub) merged with the pod's flattened income claims (pod-runtime /claims/mattr,
# purpose credential-issuance). Fields land at claims.*: given_name, family_name, birth_date,
# email, resident_country, kyc_level, verified_at, age_over_18, income_annualised (NET),
# income_frequency, income_employer, income_verified_at, income_source, income_commitment.
# No defaultValue on sub: a missing sub must fail issuance, never fetch someone else's data.
# OFFERS_API_URL must be reachable from MATTR (public HTTPS).
name: cba-issuer-claims
url: ${OFFERS_API_URL}/claims/mattr
requestMethod: GET
requestParameters:
sub:
mapFrom: claims.sub
purpose:
defaultValue: credential-issuance
authorization:
type: api-key
value: '${INTERNAL_API_KEY}'