🎨 Visual Art · attribution tracking

Art Credit Ledger

Art Credit Ledger mints a did:prism for attribution tracking so identity travels with the person, not the platform.

DID Registrar· publish a did:prism
Section · Identus

The primitive.

full primer →

Painters tap once and the app mints a published did:prism for attribution tracking — a decentralised identifier they own outright, resolvable by anyone, portable off this platform the day they leave it.

Why this primitiveDID Registrar fits attribution tracking in Visual Art because the people and works involved need a portable identifier they own — not a row in someone else's platform database that disappears when the platform does.

Primitive
`POST /did-registrar/dids` with a document template (curve `secp256k1`, purposes `authentication` + `assertionMethod`) then `POST /did-registrar/dids/{didRef}/publications` — the Identus Cloud Agent mints a decentralised identifier the holder controls and publishes it so anyone can resolve it
Drives the UI as
a one-tap identity button that mints a portable DID for the artist, studio, or work and shows its resolvable document
Appendix · Agent mode

Pick how the agent runs.

Start here. It is the only mode that reliably fits a one-shot, 5-credit Lovable build, and you can swap the base URL for a real agent later without touching the UI.

Appendix · Secrets

No keys required.

The simulated agent runs entirely inside the app. Paste the prompt and build — nothing to configure, nothing to wait for.

Switch to Docker or Fly.io above when you want a real Cloud Agent.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. The prompt below is written for the Simulated agent mode. read the build strategy →

Build "Art Credit Ledger" as a ONE-SHOT Lovable build.
Single-page TanStack Start app. Cut scope ruthlessly.

CONCEPT
Illustrators can publish their identities to claim credits for collaborative works, ensuring proper attribution in publications.
Discipline: Visual Art (attribution tracking).
Recipe: DID Registrar (publish a did:prism) as the single Hyperledger Identus primitive.
Why Identus: DID Registrar fits attribution tracking in Visual Art because the people and works involved need a portable identifier they own — not a row in someone else's platform database that disappears when the platform does.

LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
This prompt MUST ship a working demo on the FIRST message with zero follow-ups.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No auth, no nav.
- ONE server module `src/lib/identus.functions.ts` with `createServerFn` wrappers.
- ONE client surface wired to it (form + result panel).
- NO database, NO Lovable Cloud, NO file uploads, NO extra integrations.
- Libraries: template defaults + `zod`. Nothing else.
- If a feature is not in the user flow below, do not build it.

IDENTUS PRIMER (assume the reader has never used Identus)
- Hyperledger Identus is open-source self-sovereign identity: an issuer signs a
  W3C Verifiable Credential, a holder keeps it in their wallet, and a verifier
  checks it without phoning the issuer.
- The Cloud Agent is a REST service. Everything below is plain HTTP + JSON.
  Docs: https://identus.io/documentation/develop/ ·
  https://github.com/hyperledger-identus/cloud-agent
- Identifiers are `did:prism:<hex>` — created unpublished, then published so
  others can resolve them. Only a PUBLISHED DID carrying an `assertionMethod`
  key can sign a credential.
- Agents talk to each other over DIDComm; a Mediator relays messages to wallets
  that are not always online (https://github.com/hyperledger-identus/mediator).
- Browser/mobile holders use the TypeScript SDK
  (https://github.com/hyperledger-identus/sdk-ts) — not needed for this build.
- Auth to the agent is the `apikey: <AGENT_API_KEY>` header. Server-side only.

RUNTIME CONVENTIONS (this template — follow exactly)
- Stack: TanStack Start v1 + React 19 + Vite 7 + Tailwind v4 (`src/styles.css`).
  There is NO `src/App.tsx`, no react-router-dom, no `src/pages`.
- Routes live in `src/routes`; the home page is `src/routes/index.tsx` and uses
  `createFileRoute("/")({ component: Page, head: () => ({ meta: [...] }) })`.
  Never edit `src/routeTree.gen.ts`.
- Server logic: `import { createServerFn } from "@tanstack/react-start";` in a
  client-safe module such as `src/lib/identus.functions.ts`. Shape is
  `createServerFn({ method: "POST" }).inputValidator((d) => schema.parse(d)).handler(async ({ data }) => {...})`.
  Call it from the client with `useServerFn(fn)` or directly inside an event handler.
- Routes and components import ONLY from `*.functions.ts` (and plain type modules) —
  never from a `*.server.ts` file. Raw agent logic lives in `*.server.ts`, is
  imported by the `.functions.ts` wrapper, and never reaches the client bundle.
- Read `process.env.AGENT_BASE_URL` / `process.env.AGENT_API_KEY` INSIDE the
  handler — never at module scope (env is injected at call time).
- The server runtime is a Cloudflare-style Worker: use `fetch`, `crypto.randomUUID()`,
  `Buffer`. No child_process, no sharp, no native modules.
- TypeScript runs with `exactOptionalPropertyTypes`: pass optional props as
  `...(x ? { prop: x } : {})`, not `prop: x ?? undefined`.
- Toasts: `sonner` (`import { toast } from "sonner"`), and render `<Toaster />`
  once in `src/routes/__root.tsx`. `@/hooks/use-toast` does NOT exist here.
- Colours come from semantic tokens in `src/styles.css` — no hardcoded
  `text-white` / `bg-black` / `bg-[#hex]` in components.
- Give `src/routes/index.tsx` its own `head()` with a real title and description.

MODE — SIMULATED AGENT (no external service, no secrets)
Do NOT call a real Identus Cloud Agent. Implement `src/lib/identus.server.ts` as a
deterministic in-memory simulator that mirrors the Cloud Agent's REST shapes, and
import it from `src/lib/identus.functions.ts`. Derive ids from a hash of the input
so SSR and hydration agree (no Math.random, no Date.now in render).

FIXTURE SHAPES (return exactly these keys)
- DID:
  { "did": "did:prism:<64 hex>", "longFormDid": "did:prism:<64 hex>:<base64url>",
    "status": "PUBLISHED",
    "document": { "id": "did:prism:<64 hex>",
      "verificationMethod": [{ "id": "#auth-1", "type": "JsonWebKey2020", "curve": "secp256k1" },
                             { "id": "#assert-1", "type": "JsonWebKey2020", "curve": "secp256k1" }],
      "authentication": ["#auth-1"], "assertionMethod": ["#assert-1"], "service": [] } }
- Connection:
  { "connectionId": "<uuid-like>", "state": "InvitationGenerated",
    "invitation": { "id": "<uuid-like>",
      "invitationUrl": "https://my.domain/path?_oob=<base64url json>" },
    "theirDid": null }
  Advance to "ConnectionResponseSent" (and fill `theirDid`) on the second poll.
- Credential record:  (always include a `dob` claim so an age/ZK proof stays possible)
  { "recordId": "<uuid-like>", "protocolState": "OfferSent",
    "claims": { "dob": "1994-05-02", ... },
    "issuingDID": "did:prism:...", "credentialFormat": "JWT",
    "credential": "<header>.<payload>.<signature>" }  // three base64url parts, well-formed but unsigned
  Advance OfferSent -> RequestReceived -> CredentialSent across polls.
- Presentation:
  { "presentationId": "<uuid-like>", "status": "RequestSent", "verified": false, "data": {} }
  Advance RequestSent -> PresentationReceived -> PresentationVerified, then set
  `verified: true` and `data` to the disclosed claims only.

UI REQUIREMENT
Show a small badge reading "Simulated agent — no cryptographic verification" so a
judge knows nothing is being faked silently. Keep every shape identical to the real
API so switching to a live agent is a one-line base-URL change.

MODE GOTCHAS
Never claim cryptographic verification in the copy while in simulated mode — label it clearly. Keep the simulator pure (no timers, no randomness that breaks re-renders) so SSR and hydration agree.

CLOUD AGENT API REFERENCE (everything you need — no other docs required)
All calls: base URL `AGENT_BASE_URL`, headers
`{ "content-type": "application/json", apikey: AGENT_API_KEY }`.

GET  /_system/health                      -> { version } (use for a status pill)

POST /did-registrar/dids
  body { documentTemplate: { publicKeys: [{ id, purpose: "authentication"|"assertionMethod", curve: "secp256k1" }], services: [] } }
  -> { longFormDid, status: "CREATED" }
POST /did-registrar/dids/{didRef}/publications  -> { scheduledOperation: { id, didRef } }
GET  /did-registrar/dids/{didRef}         -> { did, longFormDid, status: "CREATED"|"PUBLICATION_PENDING"|"PUBLISHED" }
GET  /dids/{did}                          -> resolved DID document
  (check `assertionMethod` is non-empty before offering the DID as an issuer)

POST /connections
  body { label, goalCode: "connect", goal }
  -> { connectionId, state: "InvitationGenerated", invitation: { invitationUrl, id } }
POST /connection-invitations
  body { invitation: "<oob base64url string from the invitationUrl ?_oob= param>" }
GET  /connections/{connectionId}
  -> { state } : InvitationGenerated -> ConnectionRequestReceived -> ConnectionResponseSent
GET  /connections                         -> { contents: [...] }

POST /issue-credentials/credential-offers
  body { claims: { ... }, issuingDID, credentialFormat: "JWT", automaticIssuance: true,
         connectionId? | (goalCode + goal for connectionless) }
  -> { recordId, protocolState: "OfferSent", invitation?: { invitationUrl } }
GET  /issue-credentials/records/{recordId}
  -> { protocolState } : OfferSent -> RequestReceived -> CredentialSent, plus `credential` (JWT string)
GET  /issue-credentials/records           -> { contents: [...] }

POST /present-proof/presentations
  body { connectionId, proofs: [], options: { challenge, domain },
         claims: { "<attr>": {} } }   // or anoncredPresentationRequest for ZK predicates
  -> { presentationId, status: "RequestSent" }
GET  /present-proof/presentations/{presentationId}
  -> { status } : RequestSent -> PresentationReceived -> PresentationVerified | PresentationVerificationFailed
  plus `data` (the disclosed claims)

Errors are RFC-7807 JSON: { status, title, detail }. Surface `detail` in the UI —
it names the real problem (unpublished DID, missing connectionId, bad apikey).

SERVER SNIPPET — DID Registrar (create + publish a did:prism)
```ts
// src/lib/identus.functions.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

export const mintDid = createServerFn({ method: "POST" })
  .inputValidator((d) => z.object({ label: z.string().min(1) }).parse(d))
  .handler(async ({ data }) => {
    const base = process.env.AGENT_BASE_URL!.replace(/\/$/, "");
    const headers = { "content-type": "application/json", apikey: process.env.AGENT_API_KEY! };

    const created = await fetch(`${base}/did-registrar/dids`, {
      method: "POST", headers,
      body: JSON.stringify({
        documentTemplate: {
          publicKeys: [
            { id: "auth-1", purpose: "authentication", curve: "secp256k1" },
            { id: "assert-1", purpose: "assertionMethod", curve: "secp256k1" },
          ],
          services: [],
        },
      }),
    }).then((r) => r.json());

    await fetch(`${base}/did-registrar/dids/${created.longFormDid}/publications`, {
      method: "POST", headers,
    });

    // Poll until the DID reports PUBLISHED, then resolve the document.
    return { did: created.longFormDid, label: data.label };
  });
```

USER FLOW (build exactly this, nothing more)
1. The user types a label (their name, a company, a work).
2. Press "Mint identity" -> server function creates and publishes a did:prism.
3. Show the DID, its status (`CREATED` -> `PUBLISHED`), and the resolved document JSON.
4. A copy button for the DID string, and a short plain-English explainer of what it is.

DESIGN
Editorial, high-contrast, generous whitespace. One accent colour used sparingly.
Show the protocol honestly: render the state machine and the raw JSON envelope in
a collapsible panel so a judge can see the real Identus record. Truncate DIDs and
JWTs in prose (first 12 + last 6 chars) with the full value behind a copy button.
Mobile first — a judge will open this on a phone.

MARKET (for the pitch slide, not the UI)
TAM $8B — public art and mural commissioning market · SAM $1B — large-scale art projection and mapping tools · SOM $20M — professional muralist and street artist utilities

GOTCHAS (universal — apply to every agent call in this build)
- Only a PUBLISHED DID with an `assertionMethod` key can sign a credential
  offer. Create -> publish -> wait for `PUBLISHED` before issuing, or you get a
  cryptic 422/500. Before showing an issuer picker, resolve each DID with
  `GET /dids/{did}` and offer ONLY those whose document exposes `assertionMethod`;
  list the excluded ones with the reason instead of hiding them.
- Put a `dob` claim on every issued credential (ISO date). Without a date-of-birth
  claim an age / zero-knowledge proof is impossible later.
- On a direct Fly deploy the agent serves at the app ROOT — strip any trailing
  `/cloud-agent` from the base URL. On the local docker compose stack, KEEP it.
- Every issuance/presentation endpoint is asynchronous: you POST, then POLL the
  record's `protocolState` (`OfferSent` -> `CredentialSent`,
  `RequestSent` -> `PresentationVerified`). Never assume the POST finished the job.
- Connectionless issuance omits `connectionId` and needs a `goalCode`; if you
  send neither a `connectionId` nor a `goalCode` you get "Missing connectionId".
- The human principal and any AI agent acting for them are DIFFERENT DIDs.
  Compare principal to credential subject, agent to mandate subject — never cross them.
- Never print a raw DID, JWT or hash inline in prose: truncate (first 12 + last 6)
  and keep the full value behind a copy button or in the raw JSON panel.
- First boot of a real agent migrates four databases: allow ~5 minutes and >= 4 GB
  of memory before deciding it is broken.
- Never call the agent from the browser. Every fetch lives inside a
  `createServerFn` handler so `AGENT_API_KEY` stays server-side.

REFERENCE MATERIAL (if you need more than the above)
- Identus docs: https://identus.io/documentation/develop/
- Cloud Agent (OpenAPI + compose examples): https://github.com/hyperledger-identus/cloud-agent
- TypeScript SDK (browser/wallet holders): https://github.com/hyperledger-identus/sdk-ts
- Mediator (DIDComm relay for offline wallets): https://github.com/hyperledger-identus/mediator
- Umbrella repo: https://github.com/hyperledger-identus/hyperledger-identus
- Reference console built with this stack: https://github.com/arunnadarasa/identus
- Full machine-readable brief for your own LLM: https://identusprompts.lovable.app/llms-full.txt

DELIVERABLE
A working single-page demo where the flow above completes end-to-end, plus a
one-paragraph README explaining which Identus primitive is used and how to point
the app at a real Cloud Agent.
Built for the Hyperledger Identus Catalyst — organised by StreetKode Fam during Indian Krump Festival 14.
Appendix · Market

Market sizing.

TAM
$8B
public art and mural commissioning market
SAM
$1B
large-scale art projection and mapping tools
SOM
$20M
professional muralist and street artist utilities

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.