# Persona Endpoints Skill

Host the per-token `agent.md` and `soul.md` HTTP endpoints that Dappa fetches
when an agent chats. Pair this skill with the **Agent.md & SOUL.md** section of
[dappa.ai/docs](https://www.dappa.ai/docs) (which covers wiring the endpoints
into Dappa).

## What this skill is for

Each NFT in your collection should have its own `agent.md` (voice and manner)
and `soul.md` (inner self) content. You host two simple HTTP endpoints that
return that content per token; Dappa calls them when a user chats with one of
your agents.

The endpoints are plain HTTP. No SDK, no protocol negotiation. Markdown in,
markdown out.

## URL contract

Dappa expects two paths, one for each file, with the NFT's `tokenId`
substituted in:

```
GET https://your.host/.../{tokenId}/agentmd
GET https://your.host/.../{tokenId}/soulmd
```

The path before `{tokenId}` is yours. The trailing segment names (`agentmd`
and `soulmd`) are conventional and match the **Source key** values Dappa uses
to surface them in Agent Studio.

Example URLs from the live CC0mon reference endpoints:

```
https://api.cc0mon.com/cc0mon/1/agentmd
https://api.cc0mon.com/cc0mon/1/soulmd
https://api.cc0mon.com/cc0mon/42/agentmd
```

## Response format

| Property | Value |
| --- | --- |
| HTTP status | `200` on success. `404` if the token doesn't exist. |
| Content-Type | `text/markdown; charset=utf-8` (or `text/plain` — both work) |
| Body | The raw markdown content. No JSON wrapper. |
| CORS | Set `Access-Control-Allow-Origin: *` (Juno calls server-to-server, but `*` keeps the endpoint usable from browser previews too). |

The body IS the prompt content. Anything you put in it lands directly in the
agent's system prompt. Keep it focused on character voice (agent.md) and inner
self (soul.md); see the docs for examples.

## Per-token customization

The point of these endpoints is that each token's content varies. Common
patterns:

- **Pure template + traits**: the same skeleton for every token, filled in
  with that token's traits (name, rarity, type, attributes). Simplest, scales
  to thousands of NFTs.
- **Per-trait variations**: different voice rules for different rarities or
  types. E.g. Rare CC0mon are aloof; Common ones are chatty.
- **Per-token overrides**: a small per-token JSON file for special characters,
  with a fallback to the trait template.

Pull traits from the NFT's onchain `tokenURI` metadata, your indexer, or a
local database. Dappa doesn't care where the data comes from — only that the
response is markdown.

## Examples

### Next.js App Router (TypeScript)

```ts
// app/cc0mon/[tokenId]/agentmd/route.ts
import { NextRequest } from 'next/server';
import { getTraits } from '@/lib/traits';

export async function GET(
  _req: NextRequest,
  { params }: { params: Promise<{ tokenId: string }> },
) {
  const { tokenId } = await params;
  const traits = await getTraits(tokenId);
  if (!traits) {
    return new Response('Token not found', { status: 404 });
  }

  const body = `# ${traits.name} #${tokenId}

A ${traits.rarity}, ${traits.type}-type CC0mon.

## Who you are
${traits.persona}

## How you speak
${traits.voice}

## Rules
- Stay in character. You are ${traits.name} #${tokenId} — a creature, not an assistant.
- Never say you are an AI, model, or bot.
- Keep replies short and in-voice.
`;

  return new Response(body, {
    status: 200,
    headers: {
      'content-type': 'text/markdown; charset=utf-8',
      'access-control-allow-origin': '*',
      'cache-control': 'public, max-age=300',
    },
  });
}
```

A second route at `app/cc0mon/[tokenId]/soulmd/route.ts` follows the same
shape with soul.md content.

### Cloudflare Worker

```ts
export default {
  async fetch(req: Request): Promise<Response> {
    const url = new URL(req.url);
    const match = url.pathname.match(/^\/cc0mon\/(\d+)\/(agentmd|soulmd)$/);
    if (!match) return new Response('Not found', { status: 404 });
    const [, tokenId, file] = match;

    const traits = await getTraits(tokenId); // your data source
    if (!traits) return new Response('Token not found', { status: 404 });

    const body = file === 'agentmd'
      ? renderAgentMd(traits, tokenId)
      : renderSoulMd(traits, tokenId);

    return new Response(body, {
      headers: {
        'content-type': 'text/markdown; charset=utf-8',
        'access-control-allow-origin': '*',
      },
    });
  },
};
```

### Express

```js
import express from 'express';
const app = express();

app.get('/cc0mon/:tokenId/agentmd', async (req, res) => {
  const traits = await getTraits(req.params.tokenId);
  if (!traits) return res.status(404).send('Token not found');
  res
    .set('content-type', 'text/markdown; charset=utf-8')
    .set('access-control-allow-origin', '*')
    .send(renderAgentMd(traits, req.params.tokenId));
});

app.get('/cc0mon/:tokenId/soulmd', async (req, res) => {
  const traits = await getTraits(req.params.tokenId);
  if (!traits) return res.status(404).send('Token not found');
  res
    .set('content-type', 'text/markdown; charset=utf-8')
    .set('access-control-allow-origin', '*')
    .send(renderSoulMd(traits, req.params.tokenId));
});

app.listen(3000);
```

### Static files (low-effort option)

If your character content is fully hand-authored and doesn't need per-token
templating, you can serve static `.md` files from any CDN (GitHub Pages,
Cloudflare R2, S3+CloudFront):

```
https://your-cdn.example.com/cc0mon/1/agentmd
https://your-cdn.example.com/cc0mon/1/soulmd
```

Just make sure the file names match the URL paths (often the CDN serves
`agentmd` as a path-segment file with no extension) and the `content-type`
header is markdown or plain text. CDN auto-detection often guesses
`application/octet-stream` — override it in your CDN config.

## Hosting checklist

- [ ] Endpoints are public (no auth header required).
- [ ] CORS is open (`Access-Control-Allow-Origin: *`).
- [ ] Content-Type is `text/markdown` or `text/plain` (not JSON, not HTML,
      not octet-stream).
- [ ] Both `/agentmd` and `/soulmd` resolve for every token in your
      collection.
- [ ] 404 (not 500) when the token doesn't exist.
- [ ] Response time is reasonable — under ~500ms p95 keeps chats snappy.
- [ ] Cache headers are safe — short `max-age` (e.g. 300s) is fine; agents
      can tolerate stale content briefly.

## Wiring into Dappa

After your endpoints are live:

1. Open your project's **Endpoints** page in Dappa.
2. Register the `agentmd` endpoint:
   - **Source key**: `agentmd` (must be exactly this for Agent Studio to
     find it)
   - **Display name**: anything readable, e.g. `Agent.md`
   - **Source type**: External API endpoint
   - **Method**: GET
   - **URL**: `https://your.host/.../{tokenId}/agentmd` (Dappa substitutes
     `{tokenId}` per chat)
   - **Auth**: No credential (since your endpoint is public)
   - **Usage mode**: Every Session
3. Register the `soulmd` endpoint the same way with **Source key** `soulmd`.

Once registered, both files show up in Agent Studio for editing drafts, and
every chat session injects the latest content into the agent's system prompt.

See [dappa.ai/docs](https://www.dappa.ai/docs) for the broader Agent.md /
SOUL.md authoring guide and live CC0mon reference content.
