Wen Open Lab · open integration

Stage 3: B2B Invoices9 checks passed

Wen on Wall Street, B2B settlement
Business rules inside the token

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

RuleToken-2022 extensionEffect
Platform fee 0.25%, capped at 50 unitsTransfer FeesThe token withholds the fee on every transfer
Only approved companiesDefault Account State = FrozenNew accounts start frozen; the issuer thaws them after its own KYB checks
Invoice number mandatoryRequired 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

Wall Street meets Solana
Traditional finance, new rails
Wen market settlement desk
Invoices that cannot go missing
Meow Wen business wallet
Approved counterparties only
Stable settlement on Jupiter rails
Stable value, programmable rules
Golden Wen business coin
Every payment with its reference

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

Check: Solana Explorer · how to find the contract ID

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