✍️ Writing, Poetry & Narrative · chronology management

Narrative Timeline Archive

Pin chronological story timelines with events and character arcs permanently stored.

Pinata IPFS Pin· storage
Section · Onchain

The primitive.

full primer →

Every chronology management artefact is pinned to IPFS through Pinata; writers get a permanent CID plus a Solana transaction that timestamps it on Devnet.

Why this primitivePinata securely pins timeline JSONs to IPFS preserving story chronology over time.

Kernel
User-generated artefacts are pinned to IPFS via a Pinata JWT and referenced by CID inside the Solana transaction memo.
Drives the UI as
Every submission surfaces its CID with a live `gateway.pinata.cloud/ipfs/<cid>` preview alongside the Solana Explorer link.
Appendix · Secrets

Required keys.

PRIVY_APP_ID
Google sign-in + embedded Solana wallet (Devnet).
open ↗
PINATA_JWT
Pin images / JSON / manifests to IPFS.
open ↗
LOVABLE_API_KEY
AI calls via the Lovable AI Gateway.
open ↗
SOLANA_RPC_URL
Alchemy Solana Devnet RPC (https://solana-devnet.g.alchemy.com/v2/<KEY>). The public api.devnet.solana.com is rate-limited and CORS-blocked in the browser.
open ↗
PHANTOM_PRIVATE_KEY
Base58 secret key exported from Phantom (Settings → Export Private Key). Used ONCE by scripts/deploy.ts to pay rent + fees for your bespoke program on Devnet. Fund it from https://faucet.solana.com. Never shipped to the browser.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →

Build "Narrative Timeline Archive" in ONE Lovable message. Single-page demo on Solana Devnet. This prompt
is self-contained — do NOT rely on any external Solana skill or docs beyond what is inlined here.

CONCEPT
Pin chronological story timelines with events and character arcs permanently stored.
Discipline: Writing, Poetry & Narrative (chronology management).
Onchain primitive: Pinata IPFS Pin. Why this primitive: Pinata securely pins timeline JSONs to IPFS preserving story chronology over time.

5-CREDIT BUDGET (HARD LIMIT — one Lovable message, no follow-ups):
- ONE single-page app (index route only). No router, no Lovable Cloud, no database, no auth flows
  beyond the Privy drop-in.
- ONE bespoke Solana program deployed to Devnet FROM THE SANDBOX during this build (see DEPLOY),
  then called from the browser via Privy's embedded wallet. No Anchor framework — plain
  `solana-program` crate keeps the .so under 100 KB and rent under 0.5 SOL.
- Privy is ALWAYS the browser auth + signing layer (Google login, embedded Solana wallet on Devnet).
- Pinata/IPFS only if the idea genuinely needs to store a file or JSON metadata.
- At most ONE `LOVABLE_API_KEY` call per user action, invoked from a server function (never the browser).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets — add BEFORE sending this prompt):
- LOVABLE_API_KEY         Auto-provisioned by Lovable. One AI call per user action, max.
- PRIVY_APP_ID            From https://dashboard.privy.io. In the SAME Privy app, whitelist BOTH
                          `https://*.lovable.app` AND your published `https://<slug>.lovable.app`
                          under Allowed Origins, or the signing iframe is killed by `frame-ancestors`
                          CSP and every tx fails with a blank "Something went wrong" modal.
                          Enable "Solana gas sponsorship" for cluster `solana:devnet` in the same
                          dashboard so users never see a fund-wallet prompt.
- PINATA_JWT              Only required if the app pins media/metadata to IPFS.
                          Get one at https://app.pinata.cloud (JWT, not the legacy API key pair).
- SOLANA_RPC_URL          Alchemy Solana Devnet RPC: https://solana-devnet.g.alchemy.com/v2/<KEY>.
                          The public https://api.devnet.solana.com is rate-limited and blocks
                          browser CORS on getLatestBlockhash — do NOT use it.
- PHANTOM_PRIVATE_KEY     Base58 secret key exported from YOUR Phantom wallet (Settings -> Security
                          & Privacy -> Show Secret Recovery Phrase -> Export Private Key). Fund it
                          with ~2 Devnet SOL from https://faucet.solana.com BEFORE sending this
                          prompt. Lovable uses this key EXACTLY ONCE inside the sandbox during this
                          build to `solana program deploy` the bespoke program, then wipes it. The
                          browser bundle must never import it.

STACK
- React + Vite single page (the index route only).
- `@solana/web3.js` for tx building. `@privy-io/react-auth` v3+ for auth AND signing.
- Cluster: Devnet ONLY. Never mainnet. Use the Alchemy `SOLANA_RPC_URL` everywhere — in the
  frontend Connection, in `PrivyProvider.solana.rpcs`, and in the sandbox `solana config`.
- Devnet gas is sponsored by Privy (dashboard setting). Never call `requestAirdrop`, never render
  a "fund wallet" UI.

VITE + BUFFER POLYFILL (4 layers — omit any one and it works in dev but blanks the
published mobile build):
1. `bun add buffer vite-plugin-node-polyfills`.
2. `vite.config.ts`:
```ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default defineConfig({
  plugins: [react(), nodePolyfills({ globals: { Buffer: true, global: true }, protocolImports: true })],
  optimizeDeps: { esbuildOptions: { define: { global: "globalThis" } } },
  resolve: { alias: { "rpc-websockets/dist/lib/client": "rpc-websockets/dist/lib/client.browser.js" } },
});
```
DO NOT add a top-level `define: { global: "globalThis" }` in vite.config — it conflicts with the
plugin's Rollup inject and leaves Buffer undefined in dependency chunks. DO NOT put `import`
statements inside `build.rollupOptions.output.intro` — top-level import inside a chunk wrapper is
illegal and blanks the page.
3. `index.html` — inline BEFORE any bundle loads:
```html
<script type="module">
  import { Buffer } from "https://esm.sh/buffer@6";
  globalThis.Buffer = globalThis.Buffer || Buffer;
</script>
```
4. `src/polyfills.ts` (imported FIRST in `src/main.tsx`, before anything else):
```ts
import { Buffer } from "buffer";
(globalThis as any).Buffer = (globalThis as any).Buffer || Buffer;
(window as any).Buffer = (window as any).Buffer || Buffer;
```
Then in every file that touches Solana, add `import { Buffer } from "buffer";` at the top — the
plugin cannot inject into your own TypeScript source.

PRIVY (browser auth + Solana signing) — Privy v3, MUST include `solana.rpcs`:
Install: `bun add @privy-io/react-auth @solana/web3.js @solana/kit @solana-program/memo`
(`@solana-program/memo` is an implicit peer dep of `@privy-io/react-auth/solana`; missing it
crashes the Privy modal at runtime).

`src/components/PrivyRoot.tsx` (single-page app, no SSR concerns here, but keep imports scoped):
```tsx
import { PrivyProvider } from "@privy-io/react-auth";
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
import { createSolanaRpc, createSolanaRpcSubscriptions } from "@solana/kit";

const rpcUrl = import.meta.env.VITE_SOLANA_RPC_URL!;         // set from SOLANA_RPC_URL secret
const wsUrl  = rpcUrl.replace("https://", "wss://");

export function PrivyRoot({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={import.meta.env.VITE_PRIVY_APP_ID!}
      config={{
        loginMethods: ["google", "email"],
        embeddedWallets: { solana: { createOnLogin: "users-without-wallets" } },
        externalWallets: { solana: { connectors: toSolanaWalletConnectors() } },
        solanaClusters: [{ name: "devnet", rpcUrl }],
        solana: {
          rpcs: {
            "solana:devnet": {
              rpc: createSolanaRpc(rpcUrl),
              rpcSubscriptions: createSolanaRpcSubscriptions(wsUrl),
            },
          },
        },
      }}
    >{children}</PrivyProvider>
  );
}
```
Without the `solana.rpcs` mapping, Privy throws "No RPC configuration found for chain solana:devnet"
the moment the user tries to sign.

Expose the secrets to the browser in `.env.local` (Lovable already mirrors non-VITE secrets, but
Vite only ships `VITE_*` to the client):
```
VITE_PRIVY_APP_ID=<same value as PRIVY_APP_ID secret>
VITE_SOLANA_RPC_URL=<same value as SOLANA_RPC_URL secret>
```

Sign + send with sponsorship:
```tsx
import { useSolanaWallets, useSignAndSendTransaction } from "@privy-io/react-auth/solana";
const { wallets } = useSolanaWallets();
const embedded = wallets.find(w => w.walletClientType === "privy");
const { signAndSendTransaction } = useSignAndSendTransaction();
const { signature } = await signAndSendTransaction(tx, {
  address: embedded!.address,           // pin to embedded wallet — else Privy routes to an external one
  sponsor: true,                        // opts into the dashboard's gas sponsorship
});
```
Never call `connection.requestAirdrop` and never render a "fund your wallet" UI — sponsorship
covers everything on Devnet.

DEPLOY (Lovable runs this ONCE, inside the sandbox, during THIS build message —
it counts toward the 5-credit budget, so make it the first thing you do):

1. Create `solana/program/src/lib.rs` — a tiny native Rust program (no Anchor) with on-chain
security metadata so Explorer's Security tab shows contact + source URL even before OtterSec
verification lands:
```rust
use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
    msg, pubkey::Pubkey};
#[cfg(not(feature = "no-entrypoint"))]
use solana_security_txt::security_txt;

#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
    name: "<your-program-name>",
    project_url: "https://<your-slug>.lovable.app",
    contacts: "email:you@example.com",
    policy: "https://github.com/<user>/<repo>/blob/main/SECURITY.md",
    source_code: "https://github.com/<user>/<repo>",
    preferred_languages: "en",
    auditors: "None"
}

entrypoint!(process);
pub fn process(_pid: &Pubkey, _accs: &[AccountInfo], data: &[u8]) -> ProgramResult {
    msg!("payload: {}", std::str::from_utf8(data).unwrap_or("<binary>"));
    Ok(())
}
```
and `solana/program/Cargo.toml` with `[lib] crate-type = ["cdylib", "lib"]`,
`[features] no-entrypoint = []`, and dependencies `solana-program = "1.18"`,
`solana-security-txt = "1.1"`.

Also create `solana/program/rust-toolchain.toml` pinning the channel — this is REQUIRED for
`solana-verify --remote` to produce the same hash as on-chain:
```toml
[toolchain]
channel = "1.89.0"
```

2. In the sandbox shell (run in this exact order):
```bash
# install toolchains if missing
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
bun add bs58

# materialise the deployer keypair from the Phantom secret
node -e '
  const bs58 = require("bs58");
  const fs = require("fs");
  const secret = bs58.decode(process.env.PHANTOM_PRIVATE_KEY);
  fs.writeFileSync("/tmp/deployer.json", JSON.stringify(Array.from(secret)));
'
solana config set --url "$SOLANA_RPC_URL" --keypair /tmp/deployer.json
solana balance                                    # sanity check — must be >= 2 SOL

# build + deploy (commit Cargo.lock afterwards — verification needs it)
cargo build-sbf --manifest-path solana/program/Cargo.toml
PROGRAM_ID=$(solana program deploy \
  solana/program/target/deploy/*.so --output json | jq -r .programId)
echo "export const PROGRAM_ID = \"$PROGRAM_ID\";" > src/lib/program-id.ts

# wipe the deployer key — the browser bundle must never see it
rm -f /tmp/deployer.json
```

3. Frontend imports `PROGRAM_ID` from `src/lib/program-id.ts`. The Program ID is the only Solana
program identifier that ever reaches the browser bundle.

VERIFY (reproducible build badge on Solana Explorer) — do NOT attempt submission
from the sandbox; instead ship the artifacts so the user can run it themselves once their repo is
public on GitHub:

1. Commit `solana/program/rust-toolchain.toml` AND `solana/program/Cargo.lock` — OtterSec's remote
builder needs both to reproduce the exact on-chain hash. A missing lockfile or unpinned toolchain
returns "hash mismatch" every time.

2. Create `SECURITY.md` at the repo root with a contact email and this "How to verify" section.

3. Create `solana/program/VERIFY.md` with the exact command, program ID substituted in:
```
solana-verify verify-from-repo --remote \
  --url https://api.devnet.solana.com \
  --program-id <PROGRAM_ID> \
  --library-name <snake_case_crate_name> \
  --mount-path solana/program \
  --keypair ~/.config/solana/id.json \
  https://github.com/<user>/<repo>
```

4. Add `src/components/verification-card.tsx` that polls `https://verify.osec.io/status/<PROGRAM_ID>`
every 30s and renders "Reproducible build ✓ verified" (emerald) when `is_verified` is true,
"Verification pending" (amber) otherwise. Render it on the same page as the program demo.

Hard requirements the user must know about (put them in `VERIFY.md`):
- Repo MUST be **public** on GitHub. OtterSec clones anonymously; private repos fail with a clone
  error and Explorer will never show the green badge.
- The `~/.config/solana/id.json` on the user's machine MUST be the same upgrade authority that
  deployed the program (i.e. the Phantom key they used as `PHANTOM_PRIVATE_KEY`).
- Verification is Docker-free thanks to `--remote` (OtterSec builds server-side). The user does
  NOT need Docker installed.

CALL THE DEPLOYED PROGRAM (`src/lib/tx.ts`) — order of `keys` MUST match
your Rust `#[derive(Accounts)]` field-for-field (a swap surfaces as `AccountNotInitialized` or a bare
signature-verification error with no useful message):
```ts
import { Buffer } from "buffer";
import { Connection, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js";
import { PROGRAM_ID } from "./program-id";

export function connection() {
  return new Connection(import.meta.env.VITE_SOLANA_RPC_URL!, "confirmed");
}

export async function buildTx(payer: PublicKey, payload: string) {
  const ix = new TransactionInstruction({
    keys: [{ pubkey: payer, isSigner: true, isWritable: true }],  // order is load-bearing
    programId: new PublicKey(PROGRAM_ID),
    data: Buffer.from(payload, "utf8"),
  });
  const { blockhash } = await connection().getLatestBlockhash();
  const tx = new Transaction({ recentBlockhash: blockhash, feePayer: payer }).add(ix);
  return tx;
}
```
Do NOT install `@coral-xyz/anchor` — it drags in Node-only deps that fight Vite. Build instructions
by hand as above. Explorer link format: `https://explorer.solana.com/tx/<sig>?cluster=devnet`.

PINATA IPFS UPLOAD (`src/lib/pinata.ts`) — server function, never expose the JWT to the browser:
```ts
import { createServerFn } from "@tanstack/react-start";
export const pinToIpfs = createServerFn({ method: "POST" })
  .inputValidator((d) => d as { name: string; contentBase64: string; mime: string })
  .handler(async ({ data }) => {
    const bytes = Uint8Array.from(atob(data.contentBase64), c => c.charCodeAt(0));
    const fd = new FormData();
    fd.append("file", new Blob([bytes], { type: data.mime }), data.name);
    const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.PINATA_JWT}` },
      body: fd,
    });
    if (!res.ok) throw new Error(`pinata ${res.status}`);
    const j = await res.json();
    return { cid: j.IpfsHash as string };
  });
```
Preview URL: `https://gateway.pinata.cloud/ipfs/<cid>`.

USER FLOW
1. Land on page -> "Continue with Google" (Privy) -> embedded Solana wallet auto-provisioned,
   Devnet gas sponsored — no airdrop or funding UI.
2. On submit, upload the chronology management artefact via `pinToIpfs`, then call the bespoke program with the CID as payload via `useSignAndSendTransaction`. Render the CID, IPFS gateway preview, and Solana Explorer tx link.
3. Footer renders the credit line below.

COMMON GOTCHAS (each one has bitten prior participants — read before shipping):
- "Buffer is not defined" on published mobile only: you skipped one of the 4 polyfill layers above.
- Privy modal shows "Something went wrong" and console has a `frame-ancestors` CSP error:
  add your `*.lovable.app` origin under Privy Dashboard -> Allowed Origins for the SAME app whose
  ID is in `PRIVY_APP_ID`.
- Privy modal shows "No RPC configuration found for chain solana:devnet": you forgot the
  `solana.rpcs` mapping in `PrivyProvider` config.
- `signAndSendTransaction` succeeds but user gets a wallet-connect prompt: pass
  `{ address: embedded.address, sponsor: true }` — omitting `address` lets Privy route to an
  external wallet.
- `429 Too Many Requests` / CORS errors from `api.devnet.solana.com`: switch `SOLANA_RPC_URL` to
  Alchemy Devnet.
- `cargo build-sbf: command not found`: install the Anza CLI (see DEPLOY step 2).
- Program deploy fails with "insufficient funds for rent": the wallet backing
  `PHANTOM_PRIVATE_KEY` isn't funded — hit https://faucet.solana.com and re-run just the deploy step.
- OtterSec verification returns "hash mismatch": `rust-toolchain.toml` or `Cargo.lock` isn't
  committed, or the repo isn't public. Fix all three and re-run the `solana-verify` command.

CREDIT (must appear in UI footer AND, if you mint an NFT, inside the pinned metadata `description` field):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$320M
story planning and timeline tools
SAM
$80M
timeline management apps for writers
SOM
$12M
novelists, scriptwriters, and gamers

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

See also

Adjacent entries.