# Agent guide: PermissionControl template registry

This app talks to **PermissionControl**, an **LSP8** (identifiable digital asset / NFT collection) contract on **LUKSO** mainnet. It follows LUKSO standards: **LSP8** for the collection and per-template tokens, **LSP4** for human-readable template metadata, **LSP6** for the permission bitmask each template describes, and **ERC725Y** data keys where relevant. Templates live **on-chain** in the contract’s storage and events.

**Canonical PermissionControl (LUKSO mainnet):** `0x65Cb19C9c7b5A6695C48936Cf8917CF34438D7D3` — verify bytecode and ABI on a LUKSO block explorer before relying on this address.

**Machine-readable endpoints (no JavaScript):**

- **Skill (Markdown):** **`/skill.md`** — this guide; use `curl -s <origin>/skill.md`.
- **Contract ABI (JSON array):** **`/permission-control-abi.json`** — same ABI as this app’s `publishTemplate`, `getTemplate`, events, etc.; use `curl -s <origin>/permission-control-abi.json` for viem/ethers. Regenerated from source on each build.

Official standards (read these for correct encoding):

- [LSP8 — Identifiable Digital Asset](https://docs.lukso.tech/standards/tokens/LSP8-Identifiable-Digital-Asset)
- [LSP6 — Key Manager](https://docs.lukso.tech/standards/access-control/lsp6-key-manager)
- [LSP4 — Digital Asset Metadata](https://docs.lukso.tech/standards/tokens/LSP4-Digital-Asset-Metadata)
- [LSP2 — ERC725Y JSON Schema](https://docs.lukso.tech/standards/generic-standards/lsp2-json-schema/) (**VerifiableURI** is the correct **value** encoding for `LSP4Metadata` in ERC725Y—see below)
- [CompactBytesArray](https://docs.lukso.tech/standards/encoding/compact-bytes-array)

## What PermissionControl is

- **LSP8 collection:** Each published template is minted as a token. Indexers discover templates via **`TemplateMinted`** and reads such as **`getTemplate`**, **`getTemplateByTokenId`**, **`getLatestVersion`**, **`getTemplateTokenId`**, **`existsTemplate`**.
- **Mint fee:** **`publishTemplate`** is **payable**. Send **`msg.value` = `mintPrice()`** (read from the contract first; wei).
- **Metadata:** **`metadataURI`** in **`publishTemplate`** should be a **normal URI string** pointing at **valid LSP4 JSON**—typically **`ipfs://<cid>`** after you pin that JSON. Do **not** rely on stuffing raw JSON into the string as a substitute for IPFS; explorers and wallets expect resolvable **`ipfs://` or `https://`** metadata that matches [LSP4](https://docs.lukso.tech/standards/tokens/LSP4-Digital-Asset-Metadata)
- **ERC725Y on the LSP8 token:** When the contract (or any tool) writes the **`LSP4Metadata`** data key for the template NFT, the **value bytes** must be **LSP2 VerifiableURI**–encoded for the same URI, not raw UTF-8 string bytes. The **`metadataURI`** field in `TemplateData` / events can still be the plain **`ipfs://…` string**; only the **ERC725Y storage encoding** follows VerifiableURI. See LSP4 + LSP2 docs above.

## Pinning metadata JSON (recommended)

Use **`ipfs://…`** for **`metadataURI`**. To pin JSON without running your own Pinata keys, you can use **Forever Moments’ Pinata proxy** (same pattern as their agent docs for assets):

- **`POST https://www.forevermoments.life/api/pinata`**  
  **`multipart/form-data`** with a **`file`** field (upload your serialized **`*.json`** LSP4 document).

Response includes a CID; set:

- **`metadataURI` = `ipfs://<cid>`**

More context: Forever Moments publishes an **Agent API** (OpenAPI, Swagger, agent markdown) under their site—see **`/api/agent/v1/docs`** and **`/api/agent/v1/agents.md`** on their domain for the full operator guide (relay flows, pinning, etc.).

## Expected LSP4 JSON shape (matches this app’s publish form)

This hub builds a single root object with **`LSP4Metadata`**. Your pinned JSON should follow the same shape so the marketplace and install flow match.

Root:

```json
{
  "LSP4Metadata": {
    "name": "<display name — e.g. template title or “AppName — Template”>",
    "description": "<optional long description>",
    "links": [],
    "icon": [],
    "images": [],
    "attributes": []
  }
}
```

**`links`** (optional entries): include user-facing URLs if you have them:

| `title` | `url` | When |
|--------|--------|------|
| `Website` | app URL | If you have a public app/site |
| `Documentation` | docs URL | If you have docs |

**`attributes`** (strings; include the on-chain bytes as hex so the JSON matches what was published):

| `key` | `value` | `type` | Notes |
|-------|---------|--------|--------|
| `templateSlug` | slug | `string` | Same string you hashed to **`templateId`** |
| `templateVersion` | e.g. `1` | `string` | Must match **`version`** in `publishTemplate` |
| `chainId` | `42` | `string` | LUKSO mainnet for this deployment |
| `app` | app name | `string` | Required for display (“app” column in UIs) |
| `controllerAddress` | `0x…` | `string` | Controller that receives permissions on install |
| `publisherUP` | `0x…` | `string` | Optional; publisher’s UP |
| `primaryColor` | `#RRGGBB` | `string` | Optional brand color (six hex digits) |
| `permissions` | `0x…` (32-byte) | `string` | Same bitmask as **`publishTemplate`** `permissions` |
| `allowedCalls` | `0x…` | `string` | **Must match** the `allowedCalls` argument to `publishTemplate` byte-for-byte when you use scoped calls (see long section below). |
| `allowedERC725YDataKeys` | `0x…` | `string` | **Must match** the `allowedERC725YDataKeys` argument when you use scoped setData keys. |

**`icon` / `images`:** This hub leaves them empty arrays; you may extend with LSP4 image objects per the LSP4 spec if you add artwork later.

### Where the hub reads scoped restrictions from

- **Template detail “Allowed calls” list:** Decodes **`allowedCalls` returned by `getTemplate`** on PermissionControl — i.e. whatever you passed into **`publishTemplate`**. If this payload is **`0x`** or **cannot be decoded** into valid LSP6 tuples, the UI shows *“No scoped AllowedCalls”* even if your LSP4 JSON mentions calls in prose.
- **Install flow** can fall back to LSP4 **`template.allowedCallsHex`** only when applying permissions to the user’s UP; the **marketplace card/detail still shows on-chain bytes only**. So agents must **`publishTemplate` with correct `allowedCalls` bytes** for the UI to list scoped calls.

---

## AllowedCalls payload (replicate the publish form exactly)

This matches how this app builds **`allowedCalls`** before `publishTemplate`: logical tuples → **CompactBytesArray** ([LSP2](https://docs.lukso.tech/standards/generic-standards/lsp2-json-schema/)) → `bytes`.

### 1) Permissions vs UI panels

The publish form only collects **Allowed calls** rows when the permission bitmask includes **at least one** of:

`CALL`, `SUPER_CALL`, `STATICCALL`, `SUPER_STATICCALL`, `DELEGATECALL`, `SUPER_DELEGATECALL`, `TRANSFERVALUE`, `SUPER_TRANSFERVALUE`, `EXECUTE_RELAY_CALL`.

For **scoped** lists (concrete contracts / selectors), this hub uses **`CALL`** (not `SUPER_CALL`) together with an **AllowedCalls** list — see **scoped invariants** below. If you only set **`SUPER_CALL`** and pass **`allowedCalls: 0x`**, the contract may allow broad calls, but **there is nothing to decode** for the “Allowed calls” UI section.

### 2) Logical tuple: one LSP6 AllowedCalls element

Each on-chain element is **exactly 32 bytes**:

| Offset (bytes) | Size | Field | Details |
|----------------|------|--------|---------|
| 0–3 | 4 | **`callTypes`** | Unsigned integer, big-endian when packed into 4 bytes. Bit flags: **`1` = CALL**, **`2` = STATICCALL**, **`4` = DELEGATECALL**, **`8` = TRANSFERVALUE** (send native token with call). Combine with bitwise **OR** (e.g. CALL only → `1`; CALL + TRANSFERVALUE → `9`). |
| 4–23 | 20 | **`address`** | Target contract **`0x` + 40 hex chars** (lower case in practice). **Wildcard:** `0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF` = any address. |
| 24–27 | 4 | **`interfaceId`** | ERC-165 interface ID, 4 bytes. **Wildcard:** `0xFFFFFFFF` = any interface. Common presets: LSP7 `0xc52d6008`, LSP8 `0x3a271706`, ERC20 `0x36372b07`, ERC721 `0x80ac58cd`, ERC1155 `0xd9b67a26`. |
| 28–31 | 4 | **`functionSelector`** | Function selector, 4 bytes. **Wildcard:** `0xFFFFFFFF` = any function on that target/interface. Otherwise **`0x` + 8 hex chars** = `keccak256(signature).slice(0, 10)` (e.g. viem **`toFunctionSelector('transfer(address,uint256)')`**). |

**Multiple functions** on the **same** address + interface + callTypes: emit **one 32-byte tuple per selector** (the form expands one UI row with many signatures into many tuples).

### 3) Pack one tuple to 64 hex characters (32 bytes)

Working in hex without `0x`:

1. `callTypes` → 4-byte big-endian hex (e.g. `1` → `00000001`).
2. `address` → strip `0x`, 40 hex chars lower case.
3. `interfaceId` → strip `0x`, pad to 8 hex chars.
4. `functionSelector` → strip `0x`, pad to 8 hex chars.

Concatenate: **`callTypes(8) + address(40) + interfaceId(8) + functionSelector(8)`** = **64** hex characters = 32 bytes.

### 4) CompactBytesArray wrapper (entire `allowedCalls` bytes)

For **each** 32-byte tuple:

1. Prefix with **length** as **2-byte big-endian** byte length: **`0x0020`** in hex = **32** decimal → the four hex chars **`0020`**.
2. Append the **64** hex chars from step 3.

Concatenate all segments, then prefix the whole thing with **`0x`**.

**Empty list:** use **`0x`** (no bytes).

**Invalid encoding** (wrong length prefix, segment not 32 bytes, etc.) causes this app’s **`decodeAllowedCalls`** to return **zero tuples** → empty UI.

### 5) Pseudocode (same structure as the app)

```text
calls = []  // array of { callTypes, address, interfaceId, functionSelector }

// For each UI "row" / logical restriction:
bitmap = 0
if (allow CALL)            bitmap |= 1
if (allow STATICCALL)      bitmap |= 2
if (allow DELEGATECALL)    bitmap |= 4
if (allow TRANSFERVALUE)   bitmap |= 8

address = useAnyAddress ? 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF : concreteAddress
interfaceId = useAnyInterface ? 0xFFFFFFFF : concreteInterfaceId

if (useAnyFunction)
  calls.push({ callTypes: bitmap, address, interfaceId, functionSelector: 0xFFFFFFFF })
else
  for each signature string or 0xabcd1234 selector:
    selector = isHexSelector ? parseHex : toFunctionSelector(signature)
    calls.push({ callTypes: bitmap, address, interfaceId, functionSelector: selector })

hex = "0x"
for each tuple in calls:
  hex += "0020"  // length 32
  hex += pad32BE(callTypes) + address(40) + interfaceId(8) + functionSelector(8)
return hex
```

Use your stack’s **left-pad to 4 bytes** for `callTypes` and **exactly 8 hex digits** for interface + selector.

### 6) LSP4 `attributes` must mirror bytes

The publish form writes **`permissions`** as **32-byte hex** (`uint256` left-padded to 32 bytes — **66** chars including `0x`). **`allowedCalls`** and **`allowedERC725YDataKeys`** attribute values must be the **same hex strings** as passed to **`publishTemplate`**, so explorers and indexers stay consistent.

### 7) Scoped permission invariants (same as hub)

When using **scoped** AllowedCalls / AllowedERC725YDataKeys, this app clears **`SUPER_CALL`** and sets **`CALL`**, and clears **`SUPER_SETDATA`** and sets **`SETDATA`**, so restrictions apply. If you replicate that policy, match these bits in **`permissions`**.

---

## AllowedERC725YDataKeys payload (same as publish form)

Each **allowed data key prefix** is a `bytes32` (or shorter prefix conceptually; the app stores full keys). Encoding is **CompactBytesArray** with **variable-length** entries:

1. For each key hex string (with `0x`), let **`L`** = number of **bytes** (`(hex.length - 2) / 2`).
2. Prefix the key bytes with **`L`** as **2-byte big-endian** (same pattern as [CompactBytesArray](https://docs.lukso.tech/standards/encoding/compact-bytes-array)): pad `L` to 2 bytes, then append key without `0x`.
3. Concatenate all entries; final value is `0x`-prefixed hex.

**Empty:** **`0x`**.

---

## Publishing a new template

1. **Network:** LUKSO mainnet, **`chainId` 42**.
2. **Contract address:** **`0x65Cb19C9c7b5A6695C48936Cf8917CF34438D7D3`** (verify on-chain).
3. **Template id:** Choose a **slug**. On-chain **`templateId` = `keccak256`** over the slug bytes (typical tooling: **`keccak256(toHex(slug))`** with UTF-8 slug bytes).
4. **Encode permissions:** **`permissions`** — **`uint256`** LSP6 bitmask per [LSP6](https://docs.lukso.tech/standards/access-control/lsp6-key-manager).
5. **Encode restrictions:** Build **`allowedCalls`** and **`allowedERC725YDataKeys`** exactly as above, or **`0x`** if unused.
6. **Build LSP4 JSON**, pin to IPFS, set **`metadataURI` = `ipfs://…`** as above.
7. **Call** **`publishTemplate(templateId, version, chainId, permissions, allowedCalls, allowedERC725YDataKeys, metadataURI)`** with **`value` = `mintPrice()`**.
8. **From a Universal Profile:** Encode **`publishTemplate`** calldata, then **`UP.execute`**: operation **`0`**, **`to`** = PermissionControl, **`value`** = mint fee, **`data`** = calldata.

## After publishing

- **Deprecate:** **`deprecateTemplate(tokenId, deprecated)`** when retiring a version (per contract rules).
- **End users:** This hub **installs** a template by applying the same LSP6 bits and allowed-call / allowed-key rules the template defines.

## ABI (JSON)

Prefer fetching the ABI from this deployment (always matches the UI):

```bash
curl -s https://<your-app-origin>/permission-control-abi.json
```

Response is a **JSON array** (standard Solidity ABI) you can pass to `viem`’s `parseAbi` / `createPublicClient` `{ abi }` or ethers `Interface`. You can still cross-check the **verified** contract on a LUKSO block explorer if you want bytecode confirmation.
