Print Edition Authenticator
Printmakers mint one Sui object per physical print in an edition, letting buyers verify edition number and authenticity.
Sui object NFT· onchain authorship
Section · Onchain
full primer →The primitive.
The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that painters can act on without web3 jargon.
Why this primitiveEach key+store object is unique on-chain, mirroring the one-of-many nature of numbered print editions perfectly.
Kernel
a Move `key + store` object minted to the creator's address, holding a Walrus blobId and a timestamp, viewable on Suiscan as a native Sui object
Drives the UI as
a 'mint to claim authorship' button that returns the objectId, the creator address, and a Suiscan link
Required keys.
SUI_DEPLOYER_KEY
Sui keypair (base64) used once by `sui client publish`. Fund the address at the Sui Testnet faucet.
open ↗SUI_WALLET_SALT
64-char random string (generate via Lovable Cloud secrets). HKDF-derives a per-user Ed25519 Sui keypair server-side.
open ↗WALRUS_PUBLISHER_URL
https://publisher.walrus-testnet.walrus.space — used only when the idea pins media to Walrus.
open ↗WALRUS_AGGREGATOR_URL
https://aggregator.walrus-testnet.walrus.space — reads blobs pinned above. Optional.
open ↗Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "Print Edition Authenticator" in ONE Lovable message. Single-page demo. Sui Testnet.
CONCEPT
Printmakers mint one Sui object per physical print in an edition, letting buyers verify edition number and authenticity.
Discipline: Visual Art (limited edition printmaking).
Onchain primitive: Sui object NFT. Why: Each key+store object is unique on-chain, mirroring the one-of-many nature of numbered print editions perfectly.
BUDGET (5 credits, hard limit)
- One page. No extra routes, no dashboards, no docs.
- Enable Lovable Cloud + Google sign-in only. No email/password, no other providers.
- One Move module (<=80 lines), published ONCE to Sui Testnet.
- All signing runs server-side in ONE createServerFn.
- Walrus only if the idea needs blob storage.
- Skip tests, skip CI, skip landing marketing.
STACK
- Whatever the Lovable session ships with (TanStack Start today; classic Vite+React works too).
- `bun add @mysten/sui zod`. No @mysten/dapp-kit, no @mysten/enoki.
- All Sui signing is server-side inside createServerFn (or the equivalent server route on the classic template).
- src/lib/walrus.ts: `PUT ${process.env.WALRUS_PUBLISHER_URL}/v1/store?epochs=5` with the file body; read via `${process.env.WALRUS_AGGREGATOR_URL}/v1/<blobId>`. Do the upload inside the same server fn (or a sibling fn) so the browser never sees keys.
AUTH + WALLET (custodial demo — be honest in the UI)
- Enable Lovable Cloud. Add Google sign-in via `supabase--configure_social_auth`
with providers=["google"], disable_providers=["email"]. No Google Cloud
console, no OAuth client — Lovable brokers it.
- Sign-in button:
import { lovable } from "@/integrations/lovable";
await lovable.auth.signInWithOAuth("google", { redirect_uri: window.location.origin });
- src/lib/sui-wallet.server.ts derives a per-user Ed25519 keypair:
import { hkdfSync } from "node:crypto";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
export function deriveKeypair(userId: string) {
const salt = process.env.SUI_WALLET_SALT!;
const seed = hkdfSync("sha256", Buffer.from(salt), Buffer.from(userId),
Buffer.from("sui-wallet:ed25519:v1"), 32);
return Ed25519Keypair.fromSecretKey(new Uint8Array(seed));
}
- UI shows the derived Sui address + Suiscan link
(https://suiscan.xyz/testnet/account/<addr>) + "Fund via Testnet faucet"
button linking to https://faucet.sui.io/?address=<addr>.
- Disclosure copy under the address card:
"Demo wallet held by the server — safe for Sui Testnet, do not use for real value."
MOVE MODULE (move/sources/print_edition_authenticator.move)
```move
/// Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
module creative::print_edition_authenticator {
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
public struct Artefact has key, store {
id: UID,
blob_id: vector<u8>,
creator: address,
minted_at: u64,
}
/// Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
public entry fun mint(blob_id: vector<u8>, ctx: &mut TxContext) {
let artefact = Artefact {
id: object::new(ctx),
blob_id,
creator: tx_context::sender(ctx),
minted_at: tx_context::epoch_timestamp_ms(ctx),
};
transfer::public_transfer(artefact, tx_context::sender(ctx));
}
}
```
PUBLISH (one-time, from the sandbox)
- Write move/Move.toml:
[package]
name = "creative"
[addresses]
creative = "0x0"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
- sui client switch --env testnet
- sui client publish --gas-budget 100000000 --json move > publish.json
- Extract packageId:
jq '.objectChanges[] | select(.type=="published") | .packageId' publish.json
- Write src/data/sui-package.json:
{ "packageId": "0x...", "module": "<mod>", "network": "testnet", "publishedAt": "<iso>" }
Import it in the server fn. Never hardcode the package id in a component.
SERVER FN (src/lib/print_edition_authenticator.functions.ts)
import { createServerFn } from "@tanstack/react-start";
import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware";
import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";
import { z } from "zod";
import pkg from "@/data/sui-package.json";
import { deriveKeypair } from "./sui-wallet.server";
const MIN_GAS_MIST = 5_000_000n;
export const submit = createServerFn({ method: "POST" })
.middleware([requireSupabaseAuth])
.inputValidator((d) => z.object({ blobId: z.string().min(1).max(512) }).parse(d))
.handler(async ({ data, context }) => {
const kp = deriveKeypair(context.userId);
const addr = kp.toSuiAddress();
const client = new SuiClient({ url: getFullnodeUrl("testnet") });
const bal = await client.getBalance({ owner: addr });
if (BigInt(bal.totalBalance) < MIN_GAS_MIST) {
return { ok: false, address: addr,
error: "Fund the wallet at https://faucet.sui.io/?address=" + addr };
}
const tx = new Transaction();
tx.moveCall({
target: `${pkg.packageId}::${pkg.module}::mint`,
arguments: [tx.pure.vector("u8", Array.from(new TextEncoder().encode(data.blobId)))],
});
const res = await client.signAndExecuteTransaction({ signer: kp, transaction: tx });
return { ok: true, address: addr, digest: res.digest,
explorer: "https://suiscan.xyz/testnet/tx/" + res.digest };
});
UI (single page)
- Signed out: hero + "Sign in with Google" button (the lovable.auth call above).
- Signed in: address card (copy button, faucet link, Suiscan account link) +
disclosure copy + <action form for limited edition printmaking> that calls the server fn via useServerFn+useMutation.
- After success: render the returned digest as a Suiscan tx link. Keep the last 5 in local state.
- Footer: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"
SECRETS (Lovable Cloud -> Secrets)
- SUI_WALLET_SALT generate 64-char random via `generate_secret`.
- SUI_DEPLOYER_KEY Sui keypair (base64) used ONCE to publish the Move package.
Fund at https://faucet.sui.io/?address=<deployer-address>.
- WALRUS_PUBLISHER_URL https://publisher.walrus-testnet.walrus.space
- WALRUS_AGGREGATOR_URL https://aggregator.walrus-testnet.walrus.space
Suiscan note: a freshly derived address shows "no results" until its first tx confirms. That is expected, not a bug.
On submit, upload the limited edition printmaking artefact to Walrus, then call `print_edition_authenticator::mint(blob_id)` server-side. Render the new objectId, Walrus aggregator preview, and Suiscan link.
Market sizing.
TAM
$4B
fine art print market
SAM
$600M
limited edition printmakers and studios
SOM
$12M
independent printmakers selling at art fairs
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
oil painting provenance
Brushstroke Genesis Ledger
Painters log each studio session's physical canvas as a timestamped Sui object, building an immutable creation diary.
color theory archivingPalette DNA Registry
Illustrators register signature color palettes as on-chain objects, establishing ownership of their unique chromatic identity.
algorithmic art seedingGenerative Seed Vault
Generative artists immutably store algorithm seeds and parameters as Sui objects, proving original authorship of outputs.
gallery inventory managementGallery Consignment Object
Gallerists track physical artwork consignment states through transferable Sui objects that record custody chain.