Stage 3: B2B Invoices9 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

In business payments the hard part is not moving money but moving it correctly: to the right counterparty, with the right reference, with fees accounted for. Token-2022 can carry those rules inside the token. This model builds a settlement token that refuses payments without an invoice number and refuses counterparties nobody approved.
The task
A group of companies settles invoices with each other. Every payment must reference an invoice, only verified companies may receive funds, and the network operator takes a small, capped fee.
The model
| Rule | Token-2022 extension | Effect |
|---|---|---|
| Platform fee 0.25%, capped at 50 units | Transfer Fees | The token withholds the fee on every transfer |
| Only approved companies | Default Account State = Frozen | New accounts start frozen; the issuer thaws them after its own KYB checks |
| Invoice number mandatory | Required Memo (on the receiving account) | A payment without a memo right before it is rejected |
The flow: the issuer creates the mint; each company opens a receiving account that requires a memo; the issuer approves (thaws) verified companies; the payer sends a memo invoice:INV-… and the transfer in the same transaction.
The code
File: lab/code/src/03-b2b-invoice.ts · Apache-2.0
// Model 3: B2B settlement token with built-in rules (Token-2022, not the WNS program itself):
// - Transfer Fees: a platform fee taken by the token itself;
// - Default Account State = Frozen: only accounts the issuer thaws (verified counterparties) can hold it;
// - Required Memo: the receiving account rejects any payment without an invoice number.
// License: Apache-2.0. Example code, not audited. Devnet first. A real stablecoin issuer is a licensed entity.
import { Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction } from '@solana/web3.js';
import {
AccountState, ExtensionType, TOKEN_2022_PROGRAM_ID,
createEnableRequiredMemoTransfersInstruction, createInitializeAccount3Instruction,
createInitializeDefaultAccountStateInstruction, createInitializeMintInstruction,
createInitializeTransferFeeConfigInstruction, createThawAccountInstruction,
createTransferCheckedWithFeeInstruction, getAccountLen, getMintLen,
} from '@solana/spl-token';
import { getConnection, loadKeypair } from './common.js';
export const DECIMALS = 6;
/** The canonical SPL Memo program that Token-2022 checks for "required memo" accounts.
* Note: newer @solana/spl-memo releases default to a different program ID, so we build the memo ourselves. */
export const SPL_MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
export function memoInstruction(text: string, signer: PublicKey): TransactionInstruction {
return new TransactionInstruction({
programId: SPL_MEMO_PROGRAM_ID,
keys: [{ pubkey: signer, isSigner: true, isWritable: false }],
data: Buffer.from(text, 'utf8'),
});
}
export const FEE_BPS = 25; // 0.25%
export const FEE_CAP = 50n * 10n ** BigInt(DECIMALS); // at most 50 units per transfer
export const settlementMintSpace = () => getMintLen([ExtensionType.TransferFeeConfig, ExtensionType.DefaultAccountState]);
export const invoiceAccountSpace = () => getAccountLen([ExtensionType.TransferFeeAmount, ExtensionType.MemoTransfer]);
/** Issuer creates the settlement mint. It keeps the freeze authority: thawing = approving a counterparty. */
export function buildSettlementMint(issuer: PublicKey, mint: PublicKey, rentLamports: number): TransactionInstruction[] {
return [
SystemProgram.createAccount({ fromPubkey: issuer, newAccountPubkey: mint, space: settlementMintSpace(), lamports: rentLamports, programId: TOKEN_2022_PROGRAM_ID }),
createInitializeTransferFeeConfigInstruction(mint, issuer, issuer, FEE_BPS, FEE_CAP, TOKEN_2022_PROGRAM_ID),
createInitializeDefaultAccountStateInstruction(mint, AccountState.Frozen, TOKEN_2022_PROGRAM_ID),
createInitializeMintInstruction(mint, DECIMALS, issuer, issuer, TOKEN_2022_PROGRAM_ID),
];
}
/** A company opens an account that demands an invoice memo on every incoming payment. */
export function buildInvoiceAccount(company: PublicKey, account: PublicKey, mint: PublicKey, rentLamports: number, requireMemo = true): TransactionInstruction[] {
return [
SystemProgram.createAccount({ fromPubkey: company, newAccountPubkey: account, space: invoiceAccountSpace(), lamports: rentLamports, programId: TOKEN_2022_PROGRAM_ID }),
createInitializeAccount3Instruction(account, mint, company, TOKEN_2022_PROGRAM_ID),
...(requireMemo ? [createEnableRequiredMemoTransfersInstruction(account, company, [], TOKEN_2022_PROGRAM_ID)] : []),
];
}
/** Issuer approves (thaws) an account after its own KYB checks off-chain. */
export function buildApproveCounterparty(issuer: PublicKey, mint: PublicKey, account: PublicKey): TransactionInstruction[] {
return [createThawAccountInstruction(account, mint, issuer, [], TOKEN_2022_PROGRAM_ID)];
}
/** Fee the token will take: 0.25% capped at 50 units. Must match the mint config exactly. */
export function expectedFee(amount: bigint): bigint {
const pct = (amount * BigInt(FEE_BPS)) / 10_000n;
return pct > FEE_CAP ? FEE_CAP : pct;
}
/** Pay an invoice: the memo instruction must come right before the transfer. */
export function buildPayInvoice(args: { mint: PublicKey; from: PublicKey; to: PublicKey; payer: PublicKey; invoiceId: string | null; amount: bigint }): TransactionInstruction[] {
return [
...(args.invoiceId ? [memoInstruction(`invoice:${args.invoiceId}`, args.payer)] : []),
createTransferCheckedWithFeeInstruction(args.from, args.mint, args.to, args.payer, args.amount, DECIMALS, expectedFee(args.amount), [], TOKEN_2022_PROGRAM_ID),
];
}
/** Example of sending one step on a real cluster (devnet by default). */
export async function createSettlementMint() {
const connection = getConnection();
const issuer = loadKeypair();
const mint = Keypair.generate();
const rent = await connection.getMinimumBalanceForRentExemption(settlementMintSpace());
await sendAndConfirmTransaction(connection, new Transaction().add(...buildSettlementMint(issuer.publicKey, mint.publicKey, rent)), [issuer, mint]);
return mint.publicKey;
}
A real pitfall found while testing: recent releases of @solana/spl-memo use a different program ID by default. Token-2022 checks for the canonical SPL Memo program (MemoSq4g…), so the code builds the memo instruction itself. Always check program IDs, not only package names.
What was verified
In LiteSVM: the mint is created with fee and frozen-by-default rules; the seller's account requires a memo; the issuer funds the buyer; a payment without an invoice number is rejected ("No memo in previous instruction"); a payment to a company that was not approved is rejected ("Account is frozen"); invoice INV-2026-0042 is paid; the invoice number is recorded on-chain; the seller receives 997.5 of 1,000 with 2.5 withheld as fee; and the fee cap holds at 50 on a 100,000 payment.
Where it is useful
- Supplier networks and marketplaces with many small invoices.
- Cross-border B2B payments that need a clear audit trail.
- Corporate treasury pilots with tokenized deposits or stablecoins (as in the Shinhan case).
Limits and risks
- Issuing a stable settlement token is regulated; this is the technical layer only.
- The issuer's freeze authority is powerful. Use a multisig and clear governance.
- A memo is public. Use invoice numbers, never invoice contents.
- Fees withheld in accounts must be harvested by the fee authority; plan that operation.
Wen in pictures





Where Wen fits in this model
The settlement token here is a stable unit issued by a regulated party; WEN is not meant to be an invoice currency. Wen's role is the proof: WEN's launch was the first mass test of the Token-2022 machinery this model uses. A community or business network can still settle small service fees or tips between members in WEN, outside the invoice flow.
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