NOUNS MONEY ↗DEVELOPER WORKSHOP · V1 / TEST RELEASEDASHBOARD ↗

OPEN DOCUMENTATION / 2 OCTOBER 2026

Build with
Nouns Money.

Collect art. Program a demo.
Know exactly where the boundary is.

THE RELEASE CONTRACT

Collection saves a preference list, with no gas.

Payment sandbox creates local test intents and demo receipts.

Real payment and redemption remain disabled.

01 / WHAT IS BUILT

A small, useful first release.

The public catalog contains 100 note designs with authentic Nouns #0–99. IDs nm100-000 through nm100-099 identify this source set. The earlier ten designs and generated visual studies belong to separate sets.

The collection and profile companion save which designs you like. Signed-in collection uses your PointCast account; anonymous collection stays in your browser. Neither creates token ownership, a funded wallet or a redeemable balance.

ART FIRST / TEST ONLY

Nouns Money remains collectible art for voluntary art trades, never sold for cash. Demo note counts have no monetary value. There is no live checkout, issuer commitment, backing asset, cash redemption, merchant network, investment return or promised airdrop.

InterfaceRuns whereWhat it does
Catalog HTTP APIPublic PointCastRead the source-100 art metadata.
Collection HTTP APISame-origin, signed-in sessionRead, collect and remove preferences in the account.
TypeScript sandboxLocal browser or Node processCreate, confirm, cancel and retrieve demo intents.
Developer dashboardThis browserInspect local sandbox activity and demo receipts.

There are no HTTP payment-intent endpoints, developer API keys or webhook delivery in this release. The OpenAPI document describes the two HTTP paths that exist; sandbox methods have their own local contract below.

02 / QUICKSTART

Run a complete demo.

The SDK source lives in packages/nouns-money-sdk in the PointCast repository. Use the source directly or the browser ES module; this release does not publish an npm package. Download the TypeScript source ↓.

Node 24 · memory store

Run from a PointCast checkout with Node 24. Save this as demo.mjs at the repository root and run node demo.mjs. State lasts for this store instance.

import {
  NounsMoneySandbox, MemorySandboxStore,
} from './packages/nouns-money-sdk/src/index.ts';

const sandbox = new NounsMoneySandbox(new MemorySandboxStore());
const intent = await sandbox.createIntent(
  { label: 'Two-note art demo', noteCount: 2, mode: 'test' },
  { idempotencyKey: 'my-demo:create:0001' },
);
const confirmed = await sandbox.confirmIntent(
  intent.id,
  { noteIds: ['nm100-000', 'nm100-001'], mode: 'test' },
  { idempotencyKey: 'my-demo:confirm:0001' },
);
console.log(confirmed.status, confirmed.receipt);
// succeeded — a local demo receipt, no settlement

The checked-in quickstart script also asserts replay, stable receipts and cancellation. Run node docs/nouns-money/quickstart.mjs.

Browser module · IndexedDB

Use this inside a <script type="module"> on PointCast. The browser ES module needs no build step. For a TypeScript app in the checkout, import packages/nouns-money-sdk/src/index.ts through your bundler. Demo state belongs to that browser origin.

import { NounsMoneySandbox } from
  '/nouns-money/sdk.js';

// Same-origin browser ES module; no build step required.
// The default store is IndexedDB, on your app's origin.
const sandbox = new NounsMoneySandbox();
const intent = await sandbox.createIntent(
  { label: 'One-note art demo', noteCount: 1, mode: 'test' },
  { idempotencyKey: 'browser:create:0001' },
);
const result = await sandbox.confirmIntent(
  intent.id,
  { noteIds: ['nm100-000'], mode: 'test' },
  { idempotencyKey: 'browser:confirm:0001' },
);
console.log(result.receipt);

Changing origin or database creates a different sandbox. Browser storage can be cleared or blocked. These receipts are local demo records, not trusted payment evidence.

Want to try the complete flow without a development checkout? Open Pay with Nouns Money, create a test intent, select demo notes, confirm and inspect the receipt in the dashboard.

03 / COLLECTION HTTP API

Real paths. Clear authentication.

Read the public catalog from any HTTP client: GET https://pointcast.xyz/nouns-money/catalog.json. For example, run curl --fail https://pointcast.xyz/nouns-money/catalog.json. The response includes schema, mode: collectible-art, count: 100, notes and a provenance path. Each note has id, name, nounId, image and svg.

Method & pathBodyResult
GET /api/me/nouns-moneyNoneCurrent signed-in account collection.
POST /api/me/nouns-money{"noteId":"nm100-000"}Add one design to the set.
DELETE /api/me/nouns-money{"noteId":"nm100-000"}Remove one design from the set.

Successful requests return HTTP 200 with ok: true, schema: pointcast.nouns-money.collection/v1, userId, storage: account, noteIds, collectedAt and updatedAt. Mutation responses include changed. An empty account can have updatedAt: null.

Mutations accept JSON containing exactly one known noteId, at most 256 bytes. Send Content-Type: application/json. The total shared account-state document is capped at 16 KiB; a collect that exceeds it returns payload-too-large. The existing signed, HttpOnly pc_session cookie identifies the account; mutation requests must have an Origin that exactly matches the request origin. Browsers set Origin. Mutations also require X-PointCast-User matching the userId from the displayed GET response. This consistency guard prevents a changed browser session from writing under a different displayed account; it is not an authentication credential. Missing or mismatched values return account-changed (409). There is no cross-origin developer write access.

Use fetch in the signed-in browser

// Run in a signed-in browser on pointcast.xyz.
const currentResponse = await fetch('/api/me/nouns-money');
const current = await currentResponse.json();
if (!currentResponse.ok) throw new Error(current.reason);

const response = await fetch('/api/me/nouns-money', {
  method: 'POST',
  credentials: 'same-origin',
  headers: {
    'Content-Type': 'application/json',
    'X-PointCast-User': current.userId,
  },
  body: JSON.stringify({ noteId: 'nm100-000' }),
});
const collection = await response.json();
if (!response.ok) throw new Error(collection.reason);
console.log(collection.storage, collection.noteIds);
// account [ ... ]
// To remove the same note: method: 'DELETE', same JSON body.

Use the SDK’s HTTP client

import { NounsMoneyClient } from
  '/nouns-money/sdk.js';

// Same-origin browser session; never copy the session cookie.
const client = new NounsMoneyClient();
const catalog = await client.getCatalog();
const displayed = await client.getCollection();
const saved = await client.collectNote(
  catalog.notes[0].id, displayed.userId,
);
const removed = await client.removeNote(
  catalog.notes[0].id, displayed.userId,
);

The client wraps the catalog and collection paths above. Authentication still comes from the same-origin session. A different base URL does not grant authentication or CORS access.

Read or download the OpenAPI 3.1 specification ↗. Never put a session cookie in source code, a URL, an example or a third-party service.

04 / LOCAL INTENT MODEL

A programmable payment rehearsal.

NounsMoneySandbox runs local methods and stores local records. mode: 'test' is required for creation and confirmation. A request for live mode fails. Confirming demo notes does not consume, move, lock or debit any collected art.

                    confirm valid notes
requires_notes ──────────────────────────→ succeeded
      │                                      │
      │ cancel                               └ stable demo receipt
      ↓
   canceled

succeeded and canceled are terminal.
{
  "schema": "pointcast.nouns-money.intent/v1",
  "id": "nmpi_<uuid>",
  "mode": "test",
  "label": "Two-note art demo",
  "noteCount": 2,
  "status": "requires_notes",
  "noteIds": [],
  "receipt": null,
  "createdAt": "<ISO-8601 timestamp>",
  "updatedAt": "<ISO-8601 timestamp>"
}
MethodContract
createIntent(input, options)Label: trimmed 1–80 characters, no control characters. Integer noteCount: 1–5. mode: test. Returns requires_notes.
confirmIntent(id, input, options)Exactly noteCount unique, valid source-100 note IDs. mode: test. Returns succeeded with a stable demo receipt.
cancelIntent(id, options)Transitions requires_notes to canceled. A succeeded intent cannot be canceled.
retrieveIntent(id)Read one intent from the current store; unknown IDs fail.
listIntents()Read the intents in the current store.

Intent IDs begin nmpi_ and use a UUID. Receipt IDs begin nmr_ and derive from that intent UUID. A succeeded intent contains a receipt with id, intentId, mode: test, label, noteCount, sorted noteIds and issuedAt. Dates use ISO 8601. A canceled intent has no receipt.

The receipt records the selected design references in a demo. It is neither a cryptographic receipt nor server-verified settlement. It grants no right to goods, cash, backing assets, tokens or later allocations.

05 / IDEMPOTENCY

Retry the action, keep the result.

Each sandbox mutation requires { idempotencyKey: "your-key-here-0001" }. Keys must contain 16–128 ASCII letters, digits, periods, underscores, colons or hyphens. Generate one key for a deliberate action and reuse it when retrying that action.

  1. Same key, same normalized operation and payload: returns the original result. A retried create produces the same intent ID and original creation snapshot. Use retrieveIntent to inspect the intent’s current state after later actions.
  2. Same key, another operation or payload: fails with idempotency_conflict. Keys share one namespace within the store.
  3. Repeated confirmation with the same note selection: preserves the receipt, even with a new key. Note order is normalized; IDs are unique and sorted.
  4. A terminal intent: cannot change to another terminal outcome or accept a different note selection. Inspect its state instead of assuming a retry can undo it.

The browser store uses IndexedDB transactions to serialize mutations across tabs using the same origin and database. The memory store protects operations using the same store instance. Neither coordinates a different device or a different browser database.

Collection HTTP mutations use set membership instead of an Idempotency-Key header. Collecting an already collected design or removing an absent design succeeds with changed: false. Removing then recollecting records a new collection timestamp. There is no quantity or balance to increase through repeated clicks.

06 / ERRORS

Handle failure before showing success.

Collection API failures return {"ok":false,"reason":"code"} with the relevant non-200 status. The collection SDK client reports API failures as api_error, with status and reason when a server response exists, and malformed responses as invalid_response.

StatusReasonAction
401unauthorizedSign in through PointCast, then retry.
403origin-not-allowedUse the same-origin browser. Mutation Origin must exactly match.
415json-requiredSend Content-Type: application/json.
413body-too-largeKeep the request body at most 256 bytes.
413payload-too-largeThe shared account-state document would exceed 16 KiB. Remove unneeded saved data before retrying.
409collection-write-conflictRefresh the account collection and retry the intended action.
409account-changedRefresh the displayed account; the expected user ID is missing or differs from the session.
400bad-bodySend valid JSON.
400invalid-note-idSend exactly one noteId field, using nm100-000 through nm100-099.
503nouns-money-collection-unavailableAccount storage is unavailable. Retry later; do not show success.

Sandbox errors

invalid_request
Fix the label, note count, unique note IDs or idempotency key.
invalid_mode
Only mode: test is supported.
not_found
Retrieve an intent ID created in this store.
invalid_state
The terminal intent cannot accept that transition.
idempotency_conflict
A key was reused for another operation or payload.
capacity_exceeded
The store is full. Preserve needed receipts before an intentional reset.
storage_unavailable
IndexedDB is unavailable. Use an explicit memory store for temporary demos.
corrupted_state
The stored state failed validation. Do not treat it as a valid receipt.

Use the code, keep the message

import { NounsMoneyError } from
  './packages/nouns-money-sdk/src/index.ts';

try {
  await sandbox.retrieveIntent(
    'nmpi_00000000-0000-4000-8000-000000000000',
  );
} catch (error) {
  if (error instanceof NounsMoneyError) {
    console.log(error.code, error.message);
    // not_found ...
  } else {
    throw error;
  }
}

NounsMoneyError exposes code and message. A retryable storage error does not mean an intent succeeded. Retrieve a known intent after recovery; an idempotency replay can establish the result of a known action.

07 / STORAGE & AUTHENTICATION

Three records, three lifetimes.

RecordLifetime & boundaryReload / switching
Signed-in art collectionStored against the authenticated PointCast account.Reload fetches that account’s set. Switching accounts loads the other account; signing out ends account access.
Anonymous art collectionDevice/browser-local preferences, with temporary memory if storage is unavailable.Reload keeps a persisted set. Clearing browser data removes it. Local preferences are not automatically imported on sign-in.
Payment sandboxIndexedDB database pointcast-nouns-money-sandbox-v1, or an explicit memory store.IndexedDB survives reload, subject to browser storage policy. It is shared by the browser origin, independent of signed-in account. Memory ends with the store.

Failed account reads show an unavailable state; they do not silently substitute device preferences as account-saved. The sandbox has no account authentication. Signing out does not erase local demo receipts, and another person using the same browser can see them. Use public demo labels; do not put secrets, customer data or payment details in a label.

A store holds at most 250 intents and 1,000 mutation keys. There is no silent eviction. Preserve any useful demo records before intentionally clearing the browser’s sandbox data. Exported or edited local files remain untrusted demo data.

Existing PointCast sign-in remains the account boundary. The generic profile-state PUT route rejects this collection slot with collection-endpoint-required; use the dedicated endpoint. The SDK does not create sessions, link wallets, ask for wallet signatures, change credentials or request spending approval.

08 / PROVENANCE & RIGHTS

Keep the source attached.

The source-100 catalog uses authentic numbered Nouns #0–99. Read the source-set provenance for file evidence and exact-source verification. Catalog IDs label collectible designs; they do not imply ownership of the corresponding NFTs. Earlier ten-design art and generated studies retain their own provenance and labels.

Nouns artwork is available under CC0; using it does not imply Nouns DAO endorsement or NFT ownership. See the official Nouns brand and art resources. The exact artwork claims apply to verified flat masters, not to generated scenes or material effects.

This release does not grant a blanket license over the repository or every third-party asset. Check the existing license or provenance of each code component, font, image and source before reuse. Keep author credits and asset-specific terms with copies. Open documentation and a public specification do not change those terms.

09 / TEZOS WORKSHOP · PROPOSED

No collector gas, with a sponsor.

Design proposal only. Today’s collection makes no blockchain operation. No Nouns Money contract, token issuance, wallet challenge, sponsor service, chain fee budget or transfer service is enabled in this release.

A possible first Tezos companion could use an issuer-funded FA2 mint to a verified recipient. FA2 supports multiple token types and defines transfer and operator behavior; a mint entrypoint is contract-specific, not guaranteed by the standard. Tezos FA2 documentation ↗

  1. Establish consent and recipient control. A future wallet challenge would bind an explicit recipient, application domain, nonce and expiry. Verify it on a server before considering issuance. Arbitrary-data signing exists in wallet tooling; the challenge and verifier still need their own reviewed implementation. Octez Connect signing reference ↗
  2. Authorize the sponsor’s operation. A reviewed, contract-specific mint could issue to the recipient from an authorized issuer account. The official SmartPy tutorial shows administrator-controlled minting to recipients; it is a reference, not a Nouns Money contract. Minting reference ↗
  3. Pay and reconcile. The sponsor would pay authorized fees and storage costs, enforce a budget and duplicate-claim rules, and reconcile the operation’s observed result before recording delivery. “No collector gas” means sponsored costs, not a zero-fee chain. Octez operation reference ↗
  4. Keep later actions separate. An issuer-sponsored mint would not automatically sponsor later transfers. Permits or relayed interactions need a compatible contract and their own authorization, replay protection and review. A future adapter must distinguish requested, submitted, confirmed and failed delivery from today’s local demo receipt.

Before activation, a separate owner authorization must cover the contract, signer, fee budget, abuse controls, recovery and real-chain testing. Issuer, backing asset, merchant network and legal setup are undecided. Nothing here promises issuance, allocation, future value, redemption or an airdrop.

Webhooks also remain proposed. A future delivery service would need authenticated events, event IDs, retries and reconciliation; no callback URL can be registered and no webhook is sent today.

10 / AGENTS & X402 · PROPOSED

Specify the job before any future payment.

Proposed service only. The implemented endpoints expose the art catalog and account preferences. There is no Nouns Money agent-service HTTP route or x402 support in this release.

A future agent-service request could bind a short scope, output constraints, one acceptance-test description, and an expiry. Approval of service work is a separate decision from any future payment. A provider would return an artifact reference and digest; verification would record its method and outcome. The user acceptance decision would remain a separate field.

Local receipt adapter · no network

The TypeScript SDK example uses local memory only. Its explicit approval input represents a local workflow step and grants no financial authority. Run the complete exportable example with node docs/nouns-money/agent-service-quickstart.mjs.

import {
  approveLocalAgentServiceJob,
  createLocalAgentServiceReceipt,
  createLocalAgentServiceRequest,
  sha256Hex,
} from './packages/nouns-money-sdk/src/index.ts';

const request = createLocalAgentServiceRequest({
  scope: 'text-report',
  constraints: { maxOutputBytes: 256, mediaType: 'text/plain' },
  acceptanceTest: 'Compare the returned bytes to local expected text.',
});

// Separate local job approval. This authorizes no payment.
const approval = approveLocalAgentServiceJob(request, { approved: true });
const expected = new TextEncoder().encode('Local result.\n');
const bytes = new TextEncoder().encode('Local result.\n');
const receipt = await createLocalAgentServiceReceipt({
  request, approval, providerId: 'local-demo',
  resultRef: 'local://artifact/result.txt', bytes,
  expectedSha256: await sha256Hex(expected),
});
console.log(JSON.stringify(receipt, null, 2));
// unsigned/unverified · acceptance pending · payment not requested

The resulting record contains pointcast.nouns-money.agent-service-receipt/v1, an artifact digest, paymentStatus: not_requested, acceptanceStatus: pending and authenticity: unsigned_unverified.

What the digest checks

verification.method is sha-256-content-bytes; the outcome compares the produced bytes with separately supplied expected bytes. A match proves byte consistency only. It does not establish truth, quality, safety, ownership, delivery to another party, or user acceptance.

Never use an unsigned local JSON receipt as proof of provider identity or payment. The request, approval and artifact references in this sample are local examples, not account, merchant, or payment records.

Possible x402 V2 adapter boundary

As checked on 2 October 2026, the official HTTP guide describes the x402 V2 exchange through PAYMENT-REQUIRED (server requirements), PAYMENT-SIGNATURE (client payment payload) and PAYMENT-RESPONSE (server settlement-attempt result). These messages can carry payment requirements and settlement information; they do not define this proposed service-receipt schema or a user-acceptance contract. Read the HTTP 402 guide ↗, client/server flow ↗, and payment-identifier extension ↗.

A future adapter would require its own approved asset, network, scheme, facilitator and settlement implementation. It would keep request approval separate from payment approval and keep service result, payment status, reconciliation and user acceptance as distinct records. Any replay-protection extension must be explicitly advertised and implemented server-side; the SDK’s local idempotency keys do not substitute for x402 payment identifiers.

No Nouns Money x402 route, automatic payer, payment signer, facilitator connection, payment credential, network setting, HTTP intent endpoint or webhook delivery is present. Do not reuse existing PointCast X402_MODE=test as a dry-run switch: it labels receipts but does not prevent the settlement call. OpenAPI includes a descriptive proposed-integration extension only, with no new HTTP paths.