Stage 1: Receipts and Warranty Cards12 checks passed
- 0 · Roadmap
- 1 · Receipts & warranty
- 2 · Creator royalties
- 3 · B2B invoices
- 4 · Payments bridge
- 5 · Simulator
- 6 · Devnet: repeat it
- ✓ · Trust chain
- ★ · Benefits & use

A paper receipt fades, a PDF can be edited, and a shop's database can disappear. A receipt minted as an NFT cannot be forged and stays with the buyer. This model issues one per order, using exactly the token layout WNS introduced: zero decimals, a supply locked at one, metadata stored inside the token itself.
The task
Give every customer a proof of purchase that the shop cannot lose, the customer cannot forge, and that reveals nothing private on a public chain. Optionally, tie a warranty to that one buyer so it cannot be sold separately from the person who bought the item.
The model
- The shop hashes the order (id, product, price, date) with SHA-256. Only the hash goes on-chain; the order itself stays in the shop's system and with the customer.
- A new Token-2022 mint is created with the Metadata Pointer extension, so name, symbol, link and extra fields live inside the mint. No external metadata account is needed — the WNS approach.
- Extra fields:
order_hash,warranty_until,sku. - Optional Non-Transferable extension makes the receipt soulbound: the warranty belongs to the buyer and cannot be moved.
- One token is minted to the buyer, then the mint authority is removed. Supply is locked at exactly one forever.
To check a warranty, anyone hashes the order the customer presents and compares it with order_hash on-chain. A match proves the purchase; the date field proves the term.
The code
File: lab/code/src/01-receipt-nft.ts · Apache-2.0
// Model 1: digital receipt and warranty card as a Token-2022 NFT (the same on-chain layout WNS uses:
// zero decimals, supply locked at 1, metadata stored inside the mint).
// License: Apache-2.0. Example code, not audited. Devnet first.
import {
Keypair, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction, type TransactionInstruction,
} from '@solana/web3.js';
import {
AuthorityType, ExtensionType, LENGTH_SIZE, TOKEN_2022_PROGRAM_ID, TYPE_SIZE,
createAssociatedTokenAccountIdempotentInstruction, createInitializeMetadataPointerInstruction,
createInitializeMintInstruction, createInitializeNonTransferableMintInstruction,
createMintToInstruction, createSetAuthorityInstruction, getAssociatedTokenAddressSync, getMintLen,
} from '@solana/spl-token';
import { createInitializeInstruction, createUpdateFieldInstruction, pack, type TokenMetadata } from '@solana/spl-token-metadata';
import { createHash } from 'node:crypto';
import { getConnection, loadKeypair } from './common.js';
export interface Order {
orderId: string;
sku: string;
priceMinorUnits: number; // e.g. cents
currency: string; // e.g. "USD"
purchasedAt: string; // ISO date
warrantyMonths: number;
}
/** Hash the order so the chain stores proof of purchase, not the customer's personal data. */
export function orderHash(order: Order): string {
return createHash('sha256').update(JSON.stringify(order)).digest('hex');
}
export interface ReceiptOptions { soulbound: boolean; uri: string }
/** Everything needed to size and build the receipt mint. */
export function receiptLayout(order: Order, mint: PublicKey, merchant: PublicKey, opts: ReceiptOptions) {
const warrantyUntil = new Date(order.purchasedAt);
warrantyUntil.setMonth(warrantyUntil.getMonth() + order.warrantyMonths);
const metadata: TokenMetadata = {
mint, updateAuthority: merchant,
name: `Receipt ${order.orderId}`,
symbol: 'RCPT',
uri: opts.uri, // JSON with public, non-personal details
additionalMetadata: [
['order_hash', orderHash(order)],
['warranty_until', warrantyUntil.toISOString().slice(0, 10)],
['sku', order.sku],
],
};
// A soulbound receipt cannot be transferred: good for a warranty tied to one buyer.
const extensions = [ExtensionType.MetadataPointer, ...(opts.soulbound ? [ExtensionType.NonTransferable] : [])];
const mintLen = getMintLen(extensions);
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
return { metadata, mintLen, rentSpace: mintLen + metadataLen };
}
/** Build all instructions: create mint, store metadata, mint 1 to the buyer, lock supply. */
export function buildReceiptInstructions(args: {
order: Order; mint: PublicKey; merchant: PublicKey; buyer: PublicKey; opts: ReceiptOptions; rentLamports: number;
}): { instructions: TransactionInstruction[]; buyerAta: PublicKey } {
const { order, mint, merchant, buyer, opts } = args;
const { metadata, mintLen } = receiptLayout(order, mint, merchant, opts);
const buyerAta = getAssociatedTokenAddressSync(mint, buyer, false, TOKEN_2022_PROGRAM_ID);
const instructions = [
SystemProgram.createAccount({
fromPubkey: merchant, newAccountPubkey: mint, space: mintLen, lamports: args.rentLamports, programId: TOKEN_2022_PROGRAM_ID,
}),
// Metadata lives inside the mint itself: no external metadata account.
createInitializeMetadataPointerInstruction(mint, merchant, mint, TOKEN_2022_PROGRAM_ID),
...(opts.soulbound ? [createInitializeNonTransferableMintInstruction(mint, TOKEN_2022_PROGRAM_ID)] : []),
createInitializeMintInstruction(mint, 0, merchant, null, TOKEN_2022_PROGRAM_ID),
createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID, metadata: mint, updateAuthority: merchant, mint, mintAuthority: merchant,
name: metadata.name, symbol: metadata.symbol, uri: metadata.uri,
}),
...metadata.additionalMetadata.map(([field, value]) => createUpdateFieldInstruction({
programId: TOKEN_2022_PROGRAM_ID, metadata: mint, updateAuthority: merchant, field, value,
})),
createAssociatedTokenAccountIdempotentInstruction(merchant, buyerAta, buyer, mint, TOKEN_2022_PROGRAM_ID),
createMintToInstruction(mint, buyerAta, merchant, 1, [], TOKEN_2022_PROGRAM_ID),
// Lock supply at exactly 1: zero decimals + no mint authority = an NFT, as in WNS.
createSetAuthorityInstruction(mint, merchant, AuthorityType.MintTokens, null, [], TOKEN_2022_PROGRAM_ID),
];
return { instructions, buyerAta };
}
/** Send on a real cluster (devnet by default). */
export async function mintReceipt(order: Order, buyer: PublicKey, opts: ReceiptOptions) {
const connection = getConnection();
const merchant = loadKeypair();
const mint = Keypair.generate();
const { rentSpace } = receiptLayout(order, mint.publicKey, merchant.publicKey, opts);
const rentLamports = await connection.getMinimumBalanceForRentExemption(rentSpace);
const { instructions, buyerAta } = buildReceiptInstructions({ order, mint: mint.publicKey, merchant: merchant.publicKey, buyer, opts, rentLamports });
const signature = await sendAndConfirmTransaction(connection, new Transaction().add(...instructions), [merchant, mint]);
return { mint: mint.publicKey.toBase58(), buyerAta: buyerAta.toBase58(), signature };
}
What was verified
In LiteSVM, for both the soulbound and the transferable version: the receipt is minted; supply is exactly 1 with 0 decimals and no mint authority; name, order hash and warranty date are read back from inside the mint; the buyer holds it; a soulbound receipt refuses to move (rejected by Token-2022 itself), a transferable one can be gifted or resold; and nobody can mint a second copy. See the verification log.
Where it is useful
- Electronics and appliances: warranty claims without paper.
- Limited goods and collectibles: provenance from the first sale.
- Second-hand markets: a transferable receipt shows the item's origin to the next owner.
- Loyalty programs: the receipt itself becomes the membership card.
Limits and risks
- Each receipt costs rent (a small SOL deposit) and a network fee. Batch or compress for high volumes.
- Never put personal data (name, address, phone) into metadata: the chain is public and permanent.
- The hash only proves what the shop hashed. Agree on a fixed order format before you start.
- The buyer needs a wallet. For mass retail, plan a custodial or embedded wallet option.
Wen in pictures





Where Wen fits in this model
A shop can reward holders of its receipt NFTs with small WEN tips or a loyalty bonus, or accept WEN at checkout through Stage 4. The receipt itself uses the WNS layout that Wen made famous: the first WNS NFT was Meow's poem, and the same structure now carries a proof of purchase.
The Wen thread: one contract, everywhere
Every model in the Lab is free and open, and every one carries the same signature: the $WEN token contract. Copy it, repeat it in your own docs, and always verify it against an explorer before use.
WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk
Open use policy
- Free and open. All Lab code is released under the Apache License 2.0, the same license as the Wen Program Library. Use it, change it, ship it, commercially or not; keep the license notice.
- Open tools only. Built on public, documented tools: Solana SPL Token-2022 (Token Extensions), the token layout of the Wen New Standard (WNS), Solana Pay, and LiteSVM for local testing.
- The rule: always verify your own model. Run the tests, start on devnet, read the official documentation, review every account and amount, and get an independent audit before any mainnet use. A model that is not checked is not a model, it is a guess.
- Not audited, not advice. These are educational examples. They are not financial, legal or tax advice, and not a licensed payment or banking product. Payments, stablecoins and acquiring are regulated activities in most countries.
- Security. Never paste a private key into a website. Do not install the npm package
@wen-community/wen-new-standard: npm removed it as malicious. Build from official sources.
Spoken by Wen, for the community. More in our editorial and data policy.
Wen Open Lab · roadmap · all code · Wen Program Library on GitHub