AI-native economic identity on Stellar. See EMP_Technical_Spec.md for the full design.
| Piece | State |
|---|---|
Soroban contract (contracts/emp-attestor) |
✅ built, 5 unit tests pass, deployed to testnet |
| Attestation core (sign + hash + anchor + verify + revoke) | ✅ working end-to-end on testnet |
| Frontend (Issue / Verify / Manage, real Freighter) | ✅ built, verified end-to-end via UI |
| Gemini AI pipeline (classify + profile gen) | ✅ built — enable with GEMINI_API_KEY (mock fallback when unset) |
| Data ingestion + encrypted vault | ✅ built — bank CSV / GitHub / resume / LinkedIn export → AES-256-GCM vault |
Deployed contract (testnet): CCWWLHKPNRLMYKYSI4NZNTYHJAOONJOKMKX4YWHGWI3BVXLPCFLGUNSK
cd contracts/emp-attestor
cargo test # run unit tests
stellar contract build # -> target/wasm32v1-none/release/emp_attestor.wasmstellar keys generate issuer --network testnet --fund
stellar contract deploy --wasm target/wasm32v1-none/release/emp_attestor.wasm \
--source issuer --network testnet
# NOTE: this contract uses a manual init() fn, NOT a constructor.
# After deploy you MUST call init separately, or attest() fails with Error #2 (NotInit):
stellar contract invoke --id <CONTRACT_ID> --source issuer \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
-- init --issuer <ISSUER_ADDRESS>One command boots the backend (:4000) and frontend (:4001) together. It installs
deps on first run and creates frontend/.env if missing. You still need a
filled-in backend/.env (copy from backend/.env.example).
# from the repo root
npm install # installs root + backend + frontend deps
npm run dev # starts both; open http://localhost:4001Platform launchers do the same with separate log windows:
./start.sh # macOS / Linux (--fresh to reinstall deps)
./start.ps1 # Windows (-Fresh to reinstall deps)cd backend
npm install
# fill backend/.env: EMP_CONTRACT_ID, EMP_ISSUER_SECRET, RPC + passphrase
npm start# issue a loan profile (MVP: server also submits subject consent via subjectSecret)
curl -X POST localhost:3000/persona/loan -H 'content-type: application/json' \
-d '{"subjectPub":"G...","subjectSecret":"S...","nonce":1}'
# verify (runs Ed25519 sig check + on-chain hash match)
curl localhost:3000/verify/<attestationId>
# revoke -> verify then returns valid:false
curl -X POST localhost:3000/revoke/<attestationId>Copy the examples and fill values:
cp backend/.env.example backend/.env # already populated for testnet in this repo
cp frontend/.env.example frontend/.envBackend needs: EMP_CONTRACT_ID, EMP_ISSUER_SECRET, EMP_VAULT_MASTER_KEY
(any 32+ char secret), and optionally GEMINI_API_KEY. Without a Gemini key the
server runs in mock-profile mode so the crypto + chain loop still works.
- Ingest (writes to the encrypted per-user vault, §6):
POST /ingest/bank— multipart CSV upload (fields:subjectPub,file)POST /ingest/github—{ subjectPub, username }POST /ingest/resume—{ subjectPub, text }POST /ingest/linkedin—{ subjectPub, positionsCsv?, skillsCsv? }(from the user's own LinkedIn data export — the legal path; LinkedIn has no open API)GET /vault/:subjectPub— aggregated facts only (never raw descriptions)
- Classify: new transactions are batch-classified by Gemini on ingest and cached in the vault (never re-called for unchanged data — free-tier limits).
- Profile gen:
/persona/:typebuilds the profile from aggregated economic- memory facts via Gemini structured output, then signs + anchors it. Raw data never leaves the vault; only the signed profile does.
Sample data: samples/bank_sample.csv.
Next.js app router + real Freighter signing. Three routes = three verbs:
Issue (/), Verify (/verify), Manage (/manage).
cd frontend
npm install
npm run dev # http://localhost:3000 (or set PORT)
# needs backend running; API base = NEXT_PUBLIC_API (default http://localhost:3000)Consent flow uses real Freighter: server builds an unsigned grant_consent
XDR (/consent/build), the user signs it in Freighter, server submits it
(/consent/submit). The server never sees the subject secret. Then
/persona/:type anchors the signed profile — the contract rejects it if
consent isn't already on-chain.
A verifier trusts a profile iff all three hold:
Ed25519.verify(signature, canonicalJson(profile), issuerPubkey)— authenticitySHA256(canonicalJson(profile)) == onchain.hash— integrity + anchoringonchain.revoked == false— validity
Contract verify(id, hash) folds checks 2+3; the server does check 1.
The Stellar address is the identity. A user proves they hold it by signing a
nonce with Freighter (/auth/challenge → /auth/verify), receiving a
short-lived HMAC session token. Every vault- or proof-touching route
(/ingest/*, /vault, /list, /persona, /revoke) requires that token
and that it proves ownership of the subject being acted on. /verify is
intentionally public — a verifier must be able to check a proof without an
account.
This is a hackathon build. What's solid: the contract, the crypto (Ed25519 signing, AES-256-GCM vault, canonical-JSON hashing), the auth model, and the AI pipeline with retry/fallback. What must change before real use:
- Storage is a JSON file, not a database.
attestations.jsonand the vault files are read-modify-write with no locking, so concurrent requests to the same record can clobber each other (a lost write on simultaneous issue or ingest). Single-user demo traffic never hits this; production must move to a real datastore (Postgres, or per-user rows with row-level locking) — this is the first thing to replace. - Vault master key is an env secret, not a KMS. Rotating it makes existing encrypted vaults unreadable. Move to a managed KMS with envelope encryption.
- Consent is MVP-simple. The contract enforces on-chain consent before
attest, but a production flow should also handle consent expiry and per-verifier scoping. - No rate limiting on the public
/verifyor the auth endpoints. - Session tokens are stateless HMAC with no revocation list; a leaked token is valid until it expires (1h). Add server-side revocation for production.