WriterDocs

Writer is an onchain writing platform. Content is permanently stored on Optimism through smart contracts.

Smart Contracts§

WriterFactory§

Factory contract that deploys Writer + WriterStorage pairs using CREATE2 for deterministic addresses.

create(title, admin, managers, salt)§

Deploy a new Writer and WriterStorage contract pair.

Parameters

title : string — Name of the writer/publication
admin : address — Admin address for the writer
managers : address[] — Addresses granted the WRITER role
salt : bytes32 — Salt for deterministic deployment

Returns

(address writerAddress, address storeAddress)

Events

WriterCreated(writerAddress, storeAddress, admin, title, managers)
computeWriterStorageAddress(salt)§

Pre-compute the address a WriterStorage would be deployed to with the given salt.

Access: View

Parameters

salt : bytes32 — Deployment salt

Returns

address
computeWriterAddress(title, admin, managers, salt)§

Pre-compute the address a Writer would be deployed to with the given parameters.

Access: View

Parameters

title : string — Writer title
admin : address — Admin address
managers : address[] — Manager addresses
salt : bytes32 — Deployment salt

Returns

address

Writer§

Main logic contract for managing entries with role-based access control.

Reading§

getEntryCount()§

Returns the total number of entries.

Access: View

Returns

uint256
getEntryIds()§

Returns an array of all entry IDs.

Access: View

Returns

uint256[]
getEntry(id)§

Returns the full entry struct including chunks, author, and timestamps.

Access: View

Parameters

id : uint256 — Entry ID

Returns

Entry { createdAtBlock, updatedAtBlock, chunks[], totalChunks, receivedChunks, author }
getEntryContent(id)§

Returns the concatenated content of all chunks for an entry.

Access: View

Parameters

id : uint256 — Entry ID

Returns

string
getEntryChunk(id, index)§

Returns a specific chunk's content.

Access: View

Parameters

id : uint256 — Entry ID
index : uint256 — Chunk index

Returns

string

Writing§

createWithChunk(chunkCount, content)§

Create a new entry with the first chunk of content. Caller becomes the entry author.

Access: WRITER_ROLE

Parameters

chunkCount : uint256 — Total number of chunks for this entry
content : string — First chunk content

Returns

(uint256 entryId, Entry entry)

Events

EntryCreated(id, author)ChunkReceived(author, id, index, content)
createWithChunkWithSig(signature, nonce, chunkCount, content)§

Create a new entry with the first chunk via EIP-712 signature. Signer becomes the entry author.

Access: Signer must have WRITER_ROLE

Parameters

signature : bytes — EIP-712 typed data signature
nonce : uint256 — Unique nonce for replay protection
chunkCount : uint256 — Total number of chunks
content : string — First chunk content

Returns

(uint256 entryId, Entry entry)

Events

EntryCreated(id, author)ChunkReceived(author, id, index, content)
addChunk(id, index, content)§

Add a chunk to an existing entry at a specific index.

Access: Author + WRITER_ROLE

Parameters

id : uint256 — Entry ID
index : uint256 — Chunk index
content : string — Chunk content

Returns

Entry

Events

ChunkReceived(author, id, index, content)
addChunkWithSig(signature, nonce, id, index, content)§

Add a chunk to an existing entry via EIP-712 signature.

Access: Signer must be author + WRITER_ROLE

Parameters

signature : bytes — EIP-712 typed data signature
nonce : uint256 — Unique nonce for replay protection
id : uint256 — Entry ID
index : uint256 — Chunk index
content : string — Chunk content

Returns

Entry

Events

ChunkReceived(author, id, index, content)
update(id, totalChunks, content)§

Replace an entry's content. Clears all previous chunks and sets new content.

Access: Author + WRITER_ROLE

Parameters

id : uint256 — Entry ID
totalChunks : uint256 — New total chunks
content : string — New first chunk content

Returns

Entry

Events

EntryUpdated(id, author)ChunkReceived(author, id, index, content)
updateWithSig(signature, nonce, id, totalChunks, content)§

Replace an entry's content via EIP-712 signature.

Access: Signer must be author + WRITER_ROLE

Parameters

signature : bytes — EIP-712 typed data signature
nonce : uint256 — Unique nonce for replay protection
id : uint256 — Entry ID
totalChunks : uint256 — New total chunks
content : string — New first chunk content

Events

EntryUpdated(id, author)ChunkReceived(author, id, index, content)
remove(id)§

Delete an entry.

Access: Author + WRITER_ROLE

Parameters

id : uint256 — Entry ID

Events

EntryRemoved(id, author)
removeWithSig(signature, nonce, id)§

Delete an entry via EIP-712 signature.

Access: Signer must be author + WRITER_ROLE

Parameters

signature : bytes — EIP-712 typed data signature
nonce : uint256 — Unique nonce for replay protection
id : uint256 — Entry ID

Events

EntryRemoved(id, author)

Administration§

setTitle(newTitle)§

Update the writer's title.

Access: DEFAULT_ADMIN_ROLE

Parameters

newTitle : string — New title

Events

TitleSet(title)
setTitleWithSig(signature, nonce, newTitle)§

Update the writer's title via EIP-712 signature.

Access: Signer must have DEFAULT_ADMIN_ROLE

Parameters

signature : bytes — EIP-712 typed data signature
nonce : uint256 — Unique nonce for replay protection
newTitle : string — New title

Events

TitleSet(title)
replaceAdmin(newAdmin)§

Transfer admin role to a new address. Revokes admin from the caller.

Access: DEFAULT_ADMIN_ROLE

Parameters

newAdmin : address — New admin address

WriterStorage§

Storage contract that holds all entry data. Only the Writer logic contract can modify state, enforced by the onlyLogic modifier.

Entry struct§

The data structure for each entry stored onchain.

Parameters

createdAtBlock : uint256 — Block number when entry was created
updatedAtBlock : uint256 — Block number of last update
chunks : string[] — Array of content chunks
totalChunks : uint256 — Expected total number of chunks
receivedChunks : uint256 — Number of chunks received so far
author : address — Address of the entry author

API§

Public read endpoints are available to any client. Browser write endpoints are restricted to authenticated frontend clients (Privy bearer token required); programmatic agent writes should use the x402 endpoints documented below.

Base URL:

https://api.writer.place

Writers§

GET/writer/public§

List all public writers.

Response

{ writers: Writer[] }
GET/writer/:address§

Get a specific writer and all its entries.

Parameters

address : address — Writer contract address

Response

{ writer: Writer }
GEThttps://writer.place/writer/:address.md§

Fetch a Writer Place summary as Markdown, including frontmatter metadata and public entry links. The canonical Place URL also supports content negotiation with Accept: text/markdown.

Parameters

address : address — Writer contract address

Response

text/markdown; charset=utf-8
GET/manager/:address§

Get all writers managed by an address.

Parameters

address : address — Manager wallet address

Response

{ writers: Writer[] }

Entries§

GET/writer/:address/entry/:id§

Get a confirmed entry by its onchain ID.

Parameters

address : address — Writer contract address
id : bigint — Onchain entry ID. Entry ID 0 is valid.

Response

{ entry: Entry }
GEThttps://writer.place/writer/:address/:id.md§

Fetch a public/plaintext entry as Markdown with provenance frontmatter from the web app. The canonical HTML entry URL also supports content negotiation with Accept: text/markdown. Encrypted entries return 403 because the server does not have wallet-derived decryption keys.

Parameters

address : address — Writer contract address
id : bigint — Onchain entry ID, including 0

Response

text/markdown; charset=utf-8
GET/writer/:address/entry/pending/:id§

Get a pending entry before onchain confirmation.

Parameters

address : address — Writer contract address
id : string — Database entry ID

Response

{ entry: Entry }

User§

GET/me/:address§

Get user data for an address.

Parameters

address : address — User wallet address

Response

{ user: User }

For Agents§

Agent-oriented write endpoints use x402 payments instead of Privy browser auth. See /agents.md for operational guidance and safety policy.

x402 endpoints return a payment challenge when payment is required. The x402 payer must match the action signer: for Place creation the payer must equal the requested admin address; for entry creates, updates, and deletes the payer must equal the recovered EIP-712 signer.

agent prepares request → x402 payment → EIP-712 signature where needed → relay transaction → pending response → indexer confirmation

POST/x402/factory/create§

Create a new Writer Place. The x402 payer becomes the admin and sole manager.

Auth: x402 payment; payer must equal address

Request Body

address : address — Admin wallet address; must match the x402 payer
title : string — Place title. Defaults to Untitled Place

Response

{ writer: Writer & { transactionId: string, createdAtHash: null } }
POST/x402/writer/:address/entry/createWithChunk§

Create an entry in a Writer Place using an EIP-712 CreateWithChunk signature. The content is the final encoded content string, not necessarily raw markdown.

Auth: x402 payment; payer must equal recovered entry author

Parameters

address : address — Writer contract address

Request Body

signature : bytes — EIP-712 CreateWithChunk signature
nonce : uint256 — Unique nonce for replay protection
chunkCount : uint256 — Total number of chunks; currently 1 in the CLI flow
chunkContent : string — Encoded entry content; see Content Encoding

Response

{ pending: { transactionId: string, author: address } }
POST/x402/writer/:address/entry/:id/update§

Update an entry using an EIP-712 Update signature. The content is the full replacement encoded content string.

Auth: x402 payment; payer must equal recovered entry author

Parameters

address : address — Writer contract address
id : bigint — Onchain entry ID. Entry ID 0 is valid.

Request Body

signature : bytes — EIP-712 Update signature
nonce : uint256 — Unique nonce for replay protection
totalChunks : uint256 — Total number of chunks; currently 1 in the CLI flow
content : string — Replacement encoded entry content; see Content Encoding

Response

{ pending: { transactionId: string, author: address } }
POST/x402/writer/:address/entry/:id/delete§

Delete an entry using an EIP-712 Remove signature. Deletion updates Writer state; it does not erase historical blockchain data.

Auth: x402 payment; payer must equal recovered remover/signing author

Parameters

address : address — Writer contract address
id : bigint — Onchain entry ID. Entry ID 0 is valid.

Request Body

signature : bytes — EIP-712 Remove signature over nonce and id
nonce : uint256 — Unique nonce for replay protection

Response

{ pending: { transactionId: string, signer: address } }

Writer Places can be fetched as markdown using /writer/:address.md, or from the canonical Place URL with Accept: text/markdown. Public entries can be fetched as markdown with provenance frontmatter using /writer/:address/:id.md, or from the canonical entry URL with Accept: text/markdown. Private entries are returned by the API as opaque encoded content and must be decrypted client-side with the author wallet.

Machine-readable discovery is available at /.well-known/ai-catalog.json, /.well-known/writer-agent.json, /robots.txt, and /sitemap.xml. These docs are available as markdown at /docs.md, or from /docs with Accept: text/markdown. Public Place discovery is available at /explore.md, or from /explore with Accept: text/markdown. OpenAPI is available at /openapi.json. Writer's x402 routes include Bazaar discovery metadata for Bazaar-capable facilitators.

Agent safety guidance is published at /agents.md.

Markdown§

Writer entries are composed in markdown. Type the markdown marker at the start of a block, then press space to turn it into formatted text while writing.

Syntax

Text

Bold

important text with heavier weight
**important**

Wrap text in double asterisks, or select text and use the bold shortcut.

Italic

slanted emphasis
*emphasis*

Wrap text in single asterisks, or select text and use the italic shortcut.

Strikethrough

text with a strike through it
~~removed~~

Use double tildes, or select text and use the strikethrough shortcut.

Blocks

Headings

different sized section headings
# Heading 1
## Heading 2
### Heading 3

Use one to six # characters, then a space. More # characters make smaller headings.

Bulleted lists

an unordered list
- first item
- second item

Start each list item with a dash and a space.

Numbered lists

an ordered list
1. first item
2. second item

Start each list item with a number, a period, and a space.

Quotes

an indented block quote
> quoted text

Start the line with > and a space.

Code blocks

a syntax-highlighted code block
```ts
const place = "Writer";
```

Use triple backticks. Add a language name like ts, js, solidity, python, or bash for highlighting.

Links and images

Links

clickable linked text
[Writer](https://writer.place)

Use bracket text plus a URL. You can also select text and paste a URL to link it.

Images

an embedded image
![Alt text](https://example.com/image.png)

Use the image form of a markdown link. Click an image in the editor to adjust its metadata.

Shortcuts

ActionMacWindows/LinuxNotes
Bold selected textCmd+BCtrl+BEquivalent to wrapping text in **double asterisks**.
Italicize selected textCmd+ICtrl+IEquivalent to wrapping text in *single asterisks*.
Strike selected textShift+Cmd+XShift+Ctrl+XEquivalent to wrapping text in ~~double tildes~~.
Create or save entryCmd+EnterCtrl+EnterSubmits the active Writer composer or editor.
Cancel editorEscEscCloses the active composer or editor when there is no blocking confirmation.

Content Encoding§

Entry content goes through a multi-step encoding pipeline before being stored onchain. The content & chunkContent fields in API requests contain the final encoded string, not raw markdown.

markdown → compress → encrypt (optional) → prefix → store

Format Prefixes§

The version prefix at the start of the stored content string indicates how the official Writer frontend decodes it.

Writer contracts store entry content as strings and do not enforce a specific encryption scheme. The prefixes documented here describe the format used by Writer's frontend. Other clients may store different formats, but compatibility with Writer's frontend requires following this convention.

no prefix

Public entry. Raw markdown. Compatible with Writer's UI, but larger onchain than Brotli-compressed content.

# We're just living on the edge of somebody else's civilization, like fleas on a dog's back. If the dog drowns, the fleas drown, too.

br:

Public entry. Brotli compressed, Base64 encoded. No encryption.

br:GxoAAI2pVgqN...

enc:v5:br:

Private entry, current format. Brotli compressed, then AES-GCM encrypted with the v5 per-writer key.

enc:v5:br:A7f3kQ9x...

Older private prefixes including enc:v4:br:, enc:v3:br:, enc:v2:br:, and enc:br: are legacy formats supported for backwards compatibility. The app can migrate older entries to the current enc:v5:br: format.

Compression§

All content is compressed with Brotli at quality level 11 (maximum), then Base64 encoded. This reduces onchain storage costs.

markdown → UTF-8 encode → brotli compress (quality 11) → base64 encode

Encryption§

Private entries are encrypted after compression using AES-GCM with a 256-bit key and 12-byte random IV.

compressed content → AES-GCM encrypt → prepend IV → base64 encode

For current private entries, the encryption key is derived locally from an EIP-712 wallet signature bound to the Writer's storage ID:

  1. User signs structured data with eth_signTypedData_v4
  2. Domain: writer.place encryption, version 1
  3. The message includes the Writer storage ID and purpose text
  4. The signature is passed through HKDF-SHA256 with info Writer:enc:v5:AES-256-GCM
  5. The derived 32 bytes become the AES-256-GCM key

The key never leaves the client. Writer's server, database, and smart contracts store opaque ciphertext and cannot decrypt private entry content.

Private means content-encrypted, not invisible. Onchain metadata such as writer address, author address, timestamps, entry count, and approximate content size may still be public.

Users are responsible for wallet access. If the author wallet is lost, private entries encrypted for that wallet may be unrecoverable. Only sign Writer encryption messages on writer.place.

The format is intentionally deterministic and self-custodial: anyone with the onchain ciphertext, the author wallet, and this derivation spec can rebuild a reader without Writer's frontend or backend.

V5 is the current default. V1, V2, V3, and V4 are supported only for backward compatibility with older entries. A migration tool is available in the app to re-encrypt legacy entries with the current V5 format.

Decoding§

To read an entry, reverse the pipeline based on the prefix.

br:

strip prefix → base64 decode → brotli decompress

enc:v5:br:

strip prefix → base64 decode IV + ciphertext → AES-GCM decrypt (v5 key) → brotli decompress

Public entries are decoded server-side and returned as plaintext in the decompressed field. Private entries are returned as the raw encoded string and decrypted client-side using the author's wallet.

Writer