✍️ Writing, Poetry & Narrative · literary agents

Agents & Authors Connect

Agents & Authors Connect pairs two wallets over DIDComm so literary agents can exchange trusted claims on a private channel.

DIDComm Connection· invitation → peer channel
Section · Identus

The primitive.

full primer →

Writers scan an invitation and two wallets settle into a private DIDComm channel for literary agents — mutually authenticated, no accounts, no email addresses handed over.

Why this primitiveA DIDComm Connection suits literary agents in Writing, Poetry & Narrative because two parties have to establish a private, mutually-authenticated channel before anything sensitive — a credit, a licence, a booking — can pass between them.

Primitive
`POST /connections` with `{ label, goalCode }` returns an out-of-band invitation URL; the other side calls `POST /connection-invitations` to accept, and both agents settle into a `ConnectionResponseSent` / `ConnectionResponseReceived` state over DIDComm
Drives the UI as
a QR code or invitation link that pairs two people's wallets into a private, mutually-authenticated channel
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 "Agents & Authors Connect" as a ONE-SHOT Lovable build.
Single-page TanStack Start app. Cut scope ruthlessly.

CONCEPT
Authors invite agents to discuss representation, verifying identities and agreements through DIDComm.
Discipline: Writing, Poetry & Narrative (literary agents).
Recipe: DIDComm Connection (invitation → peer channel) as the single Hyperledger Identus primitive.
Why Identus: A DIDComm Connection suits literary agents in Writing, Poetry & Narrative because two parties have to establish a private, mutually-authenticated channel before anything sensitive — a credit, a licence, a booking — can pass between them.

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 — DIDComm Connection (invitation -> peer channel)
```ts
export const createInvitation = 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 res = await fetch(`${base}/connections`, {
      method: "POST",
      headers: { "content-type": "application/json", apikey: process.env.AGENT_API_KEY! },
      body: JSON.stringify({ label: data.label, goalCode: "connect", goal: data.label }),
    });
    const conn = await res.json();
    // conn.invitation.invitationUrl -> render as a QR code for the wallet
    return { connectionId: conn.connectionId, invitationUrl: conn.invitation.invitationUrl, state: conn.state };
  });

export const connectionState = createServerFn({ method: "GET" })
  .inputValidator((d) => z.object({ connectionId: z.string() }).parse(d))
  .handler(async ({ data }) => { /* GET /connections/{id} -> { state } */ });
```
Poll `connectionState` every 2s in the client until `ConnectionResponseSent`.

USER FLOW (build exactly this, nothing more)
1. The user presses "Create invitation" -> server function returns an invitation URL.
2. Render it as a QR code plus a copyable link.
3. Poll the connection state every 2s and show a live status pill.
4. When the state reaches `ConnectionResponseSent`, reveal the paired peer DID.

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 $1.5B — global writing tools market · SAM $400M — thriller and procedural fiction tools · SOM $16M — legal jargon accuracy checkers

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
$1.5B
global writing tools market
SAM
$400M
thriller and procedural fiction tools
SOM
$16M
legal jargon accuracy checkers

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

See also

Adjacent entries.