PL-A01 / ONE PLATE, START TO FINISH / CHARTED FROM SOURCE PDA RECORDS / SHA-256 / ANCHOR 1.2
CLOCK SLOT STAGE EXPOSELATENTDEVELOPFOGRECORD WINDOW OPEN RECORD NEVER CLOSED
GLASS PLATE ARCHIVE

ANTE / PL-0001

HOST TESTS · 11 PASS
LATENT · NOT DEPLOYED BUILD 0.1 ANCHOR 1.2.1 / RUST
RA 00h–24hδ −90°…+90°SELECT A PLATE TO MEASURE
M-2 core / programs / ante / src / lib.rs
RUSTHOST TESTED 474 LINES · UTF-8
WHAT THIS FILE DOES

The whole program: observer records, plate exposure, development, fog, the commitment hash and the eleven unit tests.

001//! ANTE: sealed calls on Solana.002//!003//! An observer exposes a plate: a commitment to a statement they keep private004//! for now. Later they develop the plate by revealing the statement and the salt.005//! A plate that is not developed before its deadline can be fogged by anyone,006//! and the fog is counted on the observer's record. Plates are never closed.007 008use anchor_lang::prelude::*;009use solana_sha256_hasher::hashv;010 011declare_id!("GGhZ8LJzNdx14SSUaNEy3YULu7ohWkYV7vBdLXSCE95y");012 013/// Shortest development window a plate may be exposed with: one hour.014pub const MIN_WINDOW_SECONDS: i64 = 60 * 60;015/// Longest development window a plate may be exposed with: 366 days.016pub const MAX_WINDOW_SECONDS: i64 = 60 * 60 * 24 * 366;017/// Largest statement a developed plate can hold, in bytes.018pub const MAX_STATEMENT_LEN: usize = 280;019/// Domain separator mixed into every commitment so a plate hash cannot be020/// mistaken for any other SHA-256 digest of the same bytes.021pub const COMMITMENT_DOMAIN: &[u8] = b"ante-plate-v1";022 023#[program]024pub mod ante {025    use super::*;026 027    /// Opens the record of one observer. One record per wallet.028    pub fn open_observer(ctx: Context<OpenObserver>) -> Result<()> {029        let observer = &mut ctx.accounts.observer;030        observer.authority = ctx.accounts.authority.key();031        observer.plates_exposed = 0;032        observer.plates_developed = 0;033        observer.plates_fogged = 0;034        observer.opened_slot = Clock::get()?.slot;035        observer.bump = ctx.bumps.observer;036        Ok(())037    }038 039    /// Records a commitment now and fixes the deadline for developing it.040    pub fn expose_plate(041        ctx: Context<ExposePlate>,042        commitment: [u8; 32],043        window_seconds: i64,044    ) -> Result<()> {045        require!(commitment != [0; 32], AnteError::EmptyCommitment);046        validate_window(window_seconds)?;047 048        let clock = Clock::get()?;049        let observer = &mut ctx.accounts.observer;050        let plate = &mut ctx.accounts.plate;051 052        plate.observer = observer.key();053        plate.index = observer.plates_exposed;054        plate.commitment = commitment;055        plate.exposed_slot = clock.slot;056        plate.exposed_at = clock.unix_timestamp;057        plate.develop_by = clock058            .unix_timestamp059            .checked_add(window_seconds)060            .ok_or(AnteError::ArithmeticOverflow)?;061        plate.status = PlateStatus::Latent;062        plate.settled_slot = 0;063        plate.settled_at = 0;064        plate.salt = [0; 32];065        plate.statement = String::new();066        plate.bump = ctx.bumps.plate;067 068        observer.plates_exposed = observer069            .plates_exposed070            .checked_add(1)071            .ok_or(AnteError::ArithmeticOverflow)?;072 073        emit!(PlateExposed {074            observer: plate.observer,075            index: plate.index,076            commitment,077            exposed_slot: plate.exposed_slot,078            develop_by: plate.develop_by,079        });080        Ok(())081    }082 083    /// Reveals the statement. The program recomputes the commitment from the084    /// salt and the statement and only accepts an exact match.085    pub fn develop_plate(086        ctx: Context<DevelopPlate>,087        salt: [u8; 32],088        statement: String,089    ) -> Result<()> {090        let clock = Clock::get()?;091        let plate = &mut ctx.accounts.plate;092 093        check_developable(plate.status, clock.unix_timestamp, plate.develop_by)?;094        check_statement(&statement)?;095        require!(096            plate_commitment(&salt, statement.as_bytes()) == plate.commitment,097            AnteError::CommitmentMismatch098        );099 100        plate.salt = salt;101        plate.statement = statement;102        plate.status = PlateStatus::Developed;103        plate.settled_slot = clock.slot;104        plate.settled_at = clock.unix_timestamp;105 106        let observer = &mut ctx.accounts.observer;107        observer.plates_developed = observer108            .plates_developed109            .checked_add(1)110            .ok_or(AnteError::ArithmeticOverflow)?;111 112        emit!(PlateDeveloped {113            observer: plate.observer,114            index: plate.index,115            settled_slot: plate.settled_slot,116        });117        Ok(())118    }119 120    /// Marks a plate that missed its deadline as fogged. Anyone may call it,121    /// so an observer cannot keep an inconvenient plate quietly latent.122    pub fn fog_plate(ctx: Context<FogPlate>) -> Result<()> {123        let clock = Clock::get()?;124        let plate = &mut ctx.accounts.plate;125 126        check_foggable(plate.status, clock.unix_timestamp, plate.develop_by)?;127 128        plate.status = PlateStatus::Fogged;129        plate.settled_slot = clock.slot;130        plate.settled_at = clock.unix_timestamp;131 132        let observer = &mut ctx.accounts.observer;133        observer.plates_fogged = observer134            .plates_fogged135            .checked_add(1)136            .ok_or(AnteError::ArithmeticOverflow)?;137 138        emit!(PlateFogged {139            observer: plate.observer,140            index: plate.index,141            settled_slot: plate.settled_slot,142        });143        Ok(())144    }145}146 147/// The digest an observer commits to: SHA-256 over the domain separator,148/// a 32-byte salt and the exact statement bytes.149pub fn plate_commitment(salt: &[u8; 32], statement: &[u8]) -> [u8; 32] {150    hashv(&[COMMITMENT_DOMAIN, salt, statement]).to_bytes()151}152 153/// A plate must stay latent for at least an hour and at most 366 days.154pub fn validate_window(window_seconds: i64) -> Result<()> {155    require!(156        window_seconds >= MIN_WINDOW_SECONDS,157        AnteError::WindowTooShort158    );159    require!(160        window_seconds <= MAX_WINDOW_SECONDS,161        AnteError::WindowTooLong162    );163    Ok(())164}165 166/// A developed statement must be present and fit the plate.167pub fn check_statement(statement: &str) -> Result<()> {168    require!(!statement.is_empty(), AnteError::EmptyStatement);169    require!(170        statement.len() <= MAX_STATEMENT_LEN,171        AnteError::StatementTooLong172    );173    Ok(())174}175 176/// Development is allowed while the plate is latent, up to and including177/// the deadline second.178pub fn check_developable(status: PlateStatus, now: i64, develop_by: i64) -> Result<()> {179    require!(status == PlateStatus::Latent, AnteError::PlateNotLatent);180    require!(now <= develop_by, AnteError::DevelopmentWindowClosed);181    Ok(())182}183 184/// Fogging is allowed only once the deadline has passed and the plate is185/// still latent.186pub fn check_foggable(status: PlateStatus, now: i64, develop_by: i64) -> Result<()> {187    require!(status == PlateStatus::Latent, AnteError::PlateNotLatent);188    require!(now > develop_by, AnteError::DevelopmentWindowOpen);189    Ok(())190}191 192#[derive(Accounts)]193pub struct OpenObserver<'info> {194    #[account(mut)]195    pub authority: Signer<'info>,196    #[account(197        init,198        payer = authority,199        space = 8 + Observer::INIT_SPACE,200        seeds = [b"observer", authority.key().as_ref()],201        bump202    )]203    pub observer: Account<'info, Observer>,204    pub system_program: Program<'info, System>,205}206 207#[derive(Accounts)]208pub struct ExposePlate<'info> {209    #[account(mut)]210    pub authority: Signer<'info>,211    #[account(212        mut,213        seeds = [b"observer", authority.key().as_ref()],214        bump = observer.bump,215        has_one = authority216    )]217    pub observer: Account<'info, Observer>,218    #[account(219        init,220        payer = authority,221        space = 8 + Plate::INIT_SPACE,222        seeds = [b"plate", observer.key().as_ref(), &observer.plates_exposed.to_le_bytes()],223        bump224    )]225    pub plate: Account<'info, Plate>,226    pub system_program: Program<'info, System>,227}228 229#[derive(Accounts)]230pub struct DevelopPlate<'info> {231    pub authority: Signer<'info>,232    #[account(233        mut,234        seeds = [b"observer", authority.key().as_ref()],235        bump = observer.bump,236        has_one = authority237    )]238    pub observer: Account<'info, Observer>,239    #[account(240        mut,241        seeds = [b"plate", observer.key().as_ref(), &plate.index.to_le_bytes()],242        bump = plate.bump,243        has_one = observer244    )]245    pub plate: Account<'info, Plate>,246}247 248#[derive(Accounts)]249pub struct FogPlate<'info> {250    #[account(251        mut,252        seeds = [b"observer", observer.authority.as_ref()],253        bump = observer.bump254    )]255    pub observer: Account<'info, Observer>,256    #[account(257        mut,258        seeds = [b"plate", observer.key().as_ref(), &plate.index.to_le_bytes()],259        bump = plate.bump,260        has_one = observer261    )]262    pub plate: Account<'info, Plate>,263}264 265/// The public record of one wallet.266#[account]267#[derive(InitSpace)]268pub struct Observer {269    pub authority: Pubkey,270    pub plates_exposed: u32,271    pub plates_developed: u32,272    pub plates_fogged: u32,273    pub opened_slot: u64,274    pub bump: u8,275}276 277/// One sealed call. Space for the full statement is paid at exposure, so278/// developing a plate never needs a resize.279#[account]280#[derive(InitSpace)]281pub struct Plate {282    pub observer: Pubkey,283    pub index: u32,284    pub commitment: [u8; 32],285    pub exposed_slot: u64,286    pub exposed_at: i64,287    pub develop_by: i64,288    pub status: PlateStatus,289    pub settled_slot: u64,290    pub settled_at: i64,291    pub salt: [u8; 32],292    #[max_len(280)]293    pub statement: String,294    pub bump: u8,295}296 297#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, Debug, InitSpace)]298pub enum PlateStatus {299    Latent,300    Developed,301    Fogged,302}303 304#[event]305pub struct PlateExposed {306    pub observer: Pubkey,307    pub index: u32,308    pub commitment: [u8; 32],309    pub exposed_slot: u64,310    pub develop_by: i64,311}312 313#[event]314pub struct PlateDeveloped {315    pub observer: Pubkey,316    pub index: u32,317    pub settled_slot: u64,318}319 320#[event]321pub struct PlateFogged {322    pub observer: Pubkey,323    pub index: u32,324    pub settled_slot: u64,325}326 327#[error_code]328pub enum AnteError {329    #[msg("Commitment must not be zero")]330    EmptyCommitment,331    #[msg("Development window is shorter than one hour")]332    WindowTooShort,333    #[msg("Development window is longer than 366 days")]334    WindowTooLong,335    #[msg("Plate is no longer latent")]336    PlateNotLatent,337    #[msg("Development window has closed")]338    DevelopmentWindowClosed,339    #[msg("Development window is still open")]340    DevelopmentWindowOpen,341    #[msg("Statement must not be empty")]342    EmptyStatement,343    #[msg("Statement is longer than 280 bytes")]344    StatementTooLong,345    #[msg("Salt and statement do not match the commitment")]346    CommitmentMismatch,347    #[msg("Arithmetic overflow")]348    ArithmeticOverflow,349}350 351#[cfg(test)]352mod tests {353    use super::*;354 355    const SALT: [u8; 32] = [7; 32];356    const DEADLINE: i64 = 1_800_000_000;357 358    fn code_of(result: Result<()>) -> u32 {359        match result.expect_err("expected an error") {360            Error::AnchorError(error) => error.error_code_number,361            other => panic!("unexpected error: {other:?}"),362        }363    }364 365    fn code(error: AnteError) -> u32 {366        u32::from(error)367    }368 369    #[test]370    fn commitment_is_deterministic() {371        let first = plate_commitment(&SALT, b"SOL closes the week above 300");372        let second = plate_commitment(&SALT, b"SOL closes the week above 300");373        assert_eq!(first, second);374    }375 376    #[test]377    fn commitment_changes_with_salt_and_statement() {378        let base = plate_commitment(&SALT, b"rain on friday");379        assert_ne!(base, plate_commitment(&[8; 32], b"rain on friday"));380        assert_ne!(base, plate_commitment(&SALT, b"rain on saturday"));381    }382 383    #[test]384    fn commitment_is_domain_separated() {385        let plain = hashv(&[&SALT, b"rain on friday"]).to_bytes();386        assert_ne!(plain, plate_commitment(&SALT, b"rain on friday"));387    }388 389    #[test]390    fn window_bounds_are_inclusive() {391        assert!(validate_window(MIN_WINDOW_SECONDS).is_ok());392        assert!(validate_window(MAX_WINDOW_SECONDS).is_ok());393        assert_eq!(394            code_of(validate_window(MIN_WINDOW_SECONDS - 1)),395            code(AnteError::WindowTooShort)396        );397        assert_eq!(398            code_of(validate_window(MAX_WINDOW_SECONDS + 1)),399            code(AnteError::WindowTooLong)400        );401        assert_eq!(code_of(validate_window(-5)), code(AnteError::WindowTooShort));402    }403 404    #[test]405    fn statement_length_is_counted_in_bytes() {406        assert!(check_statement(&"a".repeat(MAX_STATEMENT_LEN)).is_ok());407        assert_eq!(408            code_of(check_statement(&"a".repeat(MAX_STATEMENT_LEN + 1))),409            code(AnteError::StatementTooLong)410        );411        // 141 two-byte characters are 282 bytes, two more than a plate holds.412        assert_eq!(413            code_of(check_statement(&"é".repeat(141))),414            code(AnteError::StatementTooLong)415        );416        assert_eq!(code_of(check_statement("")), code(AnteError::EmptyStatement));417    }418 419    #[test]420    fn plate_has_room_for_the_longest_statement() {421        let fixed = 32 + 4 + 32 + 8 + 8 + 8 + 1 + 8 + 8 + 32 + 1;422        assert_eq!(Plate::INIT_SPACE, fixed + 4 + MAX_STATEMENT_LEN);423    }424 425    #[test]426    fn develop_is_allowed_until_the_deadline_second() {427        assert!(check_developable(PlateStatus::Latent, DEADLINE - 1, DEADLINE).is_ok());428        assert!(check_developable(PlateStatus::Latent, DEADLINE, DEADLINE).is_ok());429        assert_eq!(430            code_of(check_developable(PlateStatus::Latent, DEADLINE + 1, DEADLINE)),431            code(AnteError::DevelopmentWindowClosed)432        );433    }434 435    #[test]436    fn settled_plates_cannot_be_developed_again() {437        for status in [PlateStatus::Developed, PlateStatus::Fogged] {438            assert_eq!(439                code_of(check_developable(status, DEADLINE - 1, DEADLINE)),440                code(AnteError::PlateNotLatent)441            );442        }443    }444 445    #[test]446    fn fog_waits_for_the_deadline_to_pass() {447        assert_eq!(448            code_of(check_foggable(PlateStatus::Latent, DEADLINE, DEADLINE)),449            code(AnteError::DevelopmentWindowOpen)450        );451        assert!(check_foggable(PlateStatus::Latent, DEADLINE + 1, DEADLINE).is_ok());452    }453 454    #[test]455    fn developed_plates_cannot_be_fogged() {456        assert_eq!(457            code_of(check_foggable(PlateStatus::Developed, DEADLINE + 1, DEADLINE)),458            code(AnteError::PlateNotLatent)459        );460        assert_eq!(461            code_of(check_foggable(PlateStatus::Fogged, DEADLINE + 1, DEADLINE)),462            code(AnteError::PlateNotLatent)463        );464    }465 466    #[test]467    fn develop_and_fog_never_overlap() {468        for now in [DEADLINE - 1, DEADLINE, DEADLINE + 1] {469            let develop = check_developable(PlateStatus::Latent, now, DEADLINE).is_ok();470            let fog = check_foggable(PlateStatus::Latent, now, DEADLINE).is_ok();471            assert!(develop != fog, "exactly one path must be open at {now}");472        }473    }474}
MEASURING ENGINE M-2 READ FROM REPOSITORYBUILD-TIME COPY
CATALOGUE 001 FROM THE PLATE STACKS

ANTE is a way for anyone to seal a statement, let it sit unseen, reveal it on time or let it fog, and keep every outcome on one public record through Solana.

01 THE PROBLEM IN PLAIN LANGUAGE

Online, the record is written after the result.

Most calls are made in places built for conversation, not for record keeping. Posts can be edited or deleted. Messages get screenshotted, cropped and reposted without context. Group chats disappear. None of this is sinister. It is simply what those tools are for.

The trouble starts when a track record matters: when people decide whom to follow, whose research to pay for or whom to believe. At that point the only evidence is whatever survived, and what survives is mostly what someone chose to keep.

Publishing every call in the open is not the answer either. Some calls are only worth something while they are private, and some people do not want to show their hand before the event. The useful line is to prove when something was said without showing what was said until its author is ready.

ANTE separates proving when you said something from showing what you said.
02 WHAT ANTE IS

ANTE is a Solana program with two kinds of record. An Observer is the public record of one wallet: how many plates it has exposed, developed and let fog. A Plate is one sealed call: a hash, the slot it was recorded in, a deadline and, later, the statement itself.

A plate can hold a price target, a match result, a launch date or a sentence about the weather. The program does not care what the statement means. It only checks that the words revealed later are exactly the words sealed earlier, and that the reveal came before the deadline.

Exposing a plate costs a transaction fee and the rent for its account. There is no token gate and nothing to stake. Plates are never closed, on purpose: a record you can delete is exactly what ANTE is trying to remove.

WHY CALL IT A PLATE

A post is something you publish now. A note is something you keep to yourself. A bare hash is a fingerprint without a story. I use the word plate because astronomical glass plates had the properties I wanted: they caught one moment, they carried the time they were taken, they could not be retouched unnoticed, and the archive kept failed exposures right next to good ones.

A plate starts latent, an image that exists but cannot be seen yet. Developing it makes the image visible. Leaving it too long fogs it. Those are the only three states the program knows.

03 WHAT A PLATE CONTAINS
COMMITMENT

A 32-byte SHA-256 digest of the domain tag ante-plate-v1, a 32-byte salt and the exact statement bytes. The salt keeps short statements like "yes" or "up" from being guessed by simply trying every option.

SLOT AND TIME

The slot and the cluster Unix timestamp at the moment of exposure. The slot is the reliable ordering. The timestamp is the validators' estimate of wall-clock time and is kept for readability.

DEADLINE

Chosen by the observer at exposure, between one hour and 366 days ahead. After that second the plate can no longer be developed.

STATUS

Latent, Developed or Fogged. The only transitions are Latent to Developed and Latent to Fogged. Nothing leads back.

STATEMENT

Up to 280 bytes, written only when the plate is developed. Space for the full statement is paid at exposure, so a reveal never depends on resizing the account.

INDEX

Each plate of an observer takes the next number in sequence, and its address is derived from that number. A missing number would show.

04 ONE PLATE FROM START TO FINISH
The chart at the top of this page is a map of this sequence. It does not show live data.

First, an observer opens their record once. The Observer account is derived from their wallet, so each wallet has exactly one.

Second, they write a statement on their own device and draw a random 32-byte salt. Only the hash of the two ever leaves the device.

Third, they expose a plate with that hash and a development window. The program rejects an empty hash and any window shorter than an hour or longer than 366 days, stamps the plate with the current slot and works out the deadline.

Fourth, nothing happens. The plate sits latent on chain. Anyone can see that it exists, when it was exposed and when it has to be developed. Nobody can see what it says.

Fifth, before the deadline, the observer develops it by sending the salt and the statement. The program recomputes the hash and accepts only an exact match. Statement and salt are then written to the plate, so anyone can repeat the check.

Finally, if the deadline passes first, anyone can fog the plate. Its status becomes Fogged and the observer's fog count goes up by one. Developed and fogged plates stay on the same record.

05 WHERE SOLANA FITS

Solana keeps the order and the count. It never sees your words until you show them.

Solana gives every plate a deterministic address derived from its observer and its number, and stamps it with a slot nobody can move afterwards. That is the whole job: an ordering that depends on neither my server, my database nor my honesty.

Anchor checks the relationships before any logic runs. The observer has to belong to the signing wallet, the plate has to belong to the observer, and both addresses have to match their seeds. The program then checks status, deadline, statement length and the hash itself.

Statements stay off chain until their author develops them. Before that, the chain holds 32 bytes that reveal nothing on their own, which is why the salt matters.

Every exposure, development and fog emits an event, so an indexer or a plain web page can follow an observer's record without trusting anyone's copy of it.

06 THE CORE RECORDS
OBSERVER

Stores the wallet that owns it, three counters (exposed, developed, fogged) and the slot it was opened in. Only that wallet can expose or develop plates on it. Anyone can apply fog.

PLATE

Stores the commitment, its number, the exposure slot and time, the deadline, the status, the slot it was settled in and, once developed, the salt and the statement.

PLATE STATUS

A one-way state: Latent, then either Developed or Fogged. No instruction resets it.

EVENTS

PlateExposed, PlateDeveloped and PlateFogged carry the observer, the plate number and the relevant slot.

ERRORS

Every refusal has a name and a readable message, from EmptyCommitment to CommitmentMismatch, so a failed transaction says why it failed.

07 WHAT EXISTS TODAY

This repository contains the Anchor program, written against anchor-lang 1.2.1, with eleven unit tests covering the commitment hash, the window limits, statement length and every status transition. The tests pass on a host build with cargo test.

The program has not been built for the Solana runtime, has not been deployed to devnet or mainnet and has not been audited. The program ID in the source belongs to a keypair I generated for that future deployment. Nothing lives at that address yet.

There is no app for exposing plates yet. Today you can read the program, run its tests and check the commitment scheme for yourself. A CI workflow that runs the same tests is included, but it has not run on a public repository yet.

The source browser on this page is built from the checked-in files at build time. A site test fails if any label on the chart points to a symbol that does not exist in those files.

The token address in the header is separate from the program. It does not prove that the program is deployed, reviewed or audited, and holding the token gives no special rights over plates.

08 THE SECURITY BOUNDARY

The program can prove that a plate was exposed in a given slot, that its statement and salt hash to the committed value, that the reveal happened before the deadline and that a missed deadline was recorded as fog. It cannot judge whether a statement was right, clear or made in good faith.

The biggest weakness is selective development. An observer can expose many plates with contradicting statements and develop only the ones that aged well. ANTE cannot stop that. What it does is refuse to let the rest disappear: every undeveloped plate eventually fogs, and the fog count sits on the same record as the hits. Someone with ten plates and nine fogs has told you something.

It also cannot connect wallets to people. One person can run many observers. A record is only as strong as the reader's reason to believe a given wallet belongs to a given person.

Timestamps come from the cluster clock, which validators estimate, so slots are the ordering to rely on. A weak salt makes a short statement guessable before it is developed, so salts have to come from a secure random source. These limits are part of the design and belong on this page.

09 QUESTIONS I AM STILL WORKING THROUGH

How should a reader weigh fog against hits? A raw ratio treats a forgotten one-hour plate like a quietly abandoned year-long one. Should the length of the window count, and who decides how?

Should anyone who knows the salt be allowed to develop a plate, so a call can still be revealed if its author disappears? Should observers be able to link wallets they control, so a record survives a lost key without inviting abuse? How should plates be shown so that a long, careful statement is not drowned out by a short, lucky one?

I would rather leave these open on the page than turn them into roadmap promises. The program, its tests and its limits are published side by side, so every claim here can be checked against the code.

10 THE DIRECTION

The next real step is to build the program for the Solana runtime, run it against a local validator with integration tests and deploy it to devnet. After that comes a small page where anyone can expose a plate from their browser, with the salt generated and kept on their own device.

Then comes the work that makes it trustworthy rather than merely working: a verifiable build, an independent review, a public indexer for the three events and a reader view that shows an observer's full record, fog included, in plain language.

I would rather ship a small record that holds up than a big platform for predictions. The long-term idea fits in one sentence: if you want credit for being early, you should be able to prove it, and you should not be able to hide the times you were wrong.

  • SOLANA
  • ANCHOR
  • COMMIT-REVEAL
  • SHA-256
  • PUBLIC RECORD

ANTE catalogue 001 / taken from the source, plate by plate / October 2026