MiCA Article Mapping

Complete article-by-article code mapping of Regulation (EU) 2023/1114 — Markets in Crypto-Assets.

Source: Regulation (EU) 2023/1114|In force: June 29, 2023|Fully applicable: December 30, 2024


Quick Reference — Key Thresholds & Deadlines

Essential MiCA numbers every implementer needs to know

ArticleThreshold / Deadline
Art. 4(2)White paper exemption: ≤ EUR 1,000,000 over 12 months
Art. 4(2)Per-MS exemption: < 150 persons per Member State
Art. 8(5)White paper notification: ≥ 20 WORKING DAYS before publication
Art. 10(1)Offer result publication: within 20 WORKING DAYS of the end of subscription
Art. 12(2)Modified white paper: notified ≥ 7 WORKING DAYS before publication
Art. 13(1)Retail withdrawal period: 14 CALENDAR DAYS
Art. 13(2)Refund deadline: 14 DAYS from withdrawal notice
Art. 14(3)Cancelled offer: funds returned within 25 CALENDAR DAYS
Art. 20(2)ART authorisation assessment: 60 WORKING DAYS
Art. 35(1)ART own funds floor: EUR 350,000
Art. 35(1)ART own funds: 2% of 6-month average reserve
Art. 45(5)Significant ART own funds: 3% of average reserve
Art. 36(4)ART deposits in credit institutions: ≥ 30% per referenced currency
Art. 45(7)Significant ART deposits: ≥ 60% per referenced currency
Art. 43Significant ART: ≥ 3 of 7 criteria
Art. 43Holders threshold: > 10,000,000
Art. 43Value threshold: > EUR 5,000,000,000
Art. 49(4)EMT redemption: at any time, at par value, no fee
Art. 51(11)EMT white paper notification: ≥ 20 WORKING DAYS before publication
Art. 54(a)EMT funds: ≥ 30% deposited in credit institutions
Art. 63(9)CASP authorisation assessment: 40 WORKING DAYS
Art. 67(1)(a), Annex IVCASP Class 1: EUR 50,000
Art. 67(1)(a), Annex IVCASP Class 2: EUR 125,000
Art. 67(1)(a), Annex IVCASP Class 3: EUR 150,000
Art. 68(9)CASP records retention: 5 YEARS
Art. 70(3)Client funds placed with a credit institution or central bank by the end of the next business day
Art. 75(5)Custody statement of position: at least every 3 MONTHS
Art. 81(12)Suitability review: at least every 2 YEARS

Article-by-Article Mapping

Title II — Crypto-Assets (not ART/EMT) — Art. 4–15

Art. 4

Offers to the Public of Crypto-Assets

View POC

A person may offer crypto-assets (other than ARTs or EMTs) to the public in the EU only if it is a legal person, has drawn up, notified and published a crypto-asset white paper, and complies with the offeror obligations — unless an Art. 4(2) exemption or Art. 4(3) exclusion applies.

Obligations
  • Offeror must be a legal person (Art. 4(1)(a))
  • Draw up a crypto-asset white paper (Art. 6)
  • Notify the white paper to the competent authority at least 20 WORKING DAYS before publication (Art. 8(5))
  • Publish the white paper on the offeror's website (Art. 9)
  • Exemptions (Art. 4(2)): < 150 persons per Member State, ≤ EUR 1,000,000 over 12 months, or qualified investors only
Key Thresholds

Exemption threshold (Art. 4(2)(b)): ≤ EUR 1,000,000 over 12 months

Per-MS exemption (Art. 4(2)(a)): < 150 persons per Member State

NCA notification (Art. 8(5)): ≥ 20 WORKING DAYS before publication

Art. 4 — TypeScript / ZodTypeScript
import { z } from "zod"

const TargetAudienceEnum = z.enum([
  "general_public",        // full Art. 4(1) requirements
  "limited_distribution",  // Art. 4(2)(a): < 150 persons per Member State
  "qualified_investors",   // Art. 4(2)(c): addressed to and holdable only by QIs
  "free_distribution",     // Art. 4(3)(a): Title II does not apply
  "mining_reward",         // Art. 4(3)(b)
  "existing_utility",      // Art. 4(3)(c): good or service exists or is in operation
  "limited_network",       // Art. 4(3)(d)
])

const OfferingDataSchema = z.object({
  offerorIsLegalEntity: z.boolean(),                          // Art. 4(1)(a)
  totalConsiderationEUR_12months: z.number().nonnegative(),   // Art. 4(2)(b): ≤ EUR 1,000,000
  targetAudience: TargetAudienceEnum,
  personsPerMemberState: z.number().int().nonnegative(),      // Art. 4(2)(a)
  holdingRestrictedToQualifiedInvestors: z.boolean(),         // Art. 4(2)(c)
  freeOfferAgainstDataOrFees: z.boolean(),  // Art. 4(3), second subparagraph: then not "free"
  utilityGoodOrServiceExists: z.boolean(),  // Art. 4(3)(c)
  intendsAdmissionToTrading: z.boolean(),   // Art. 4(4): no exemption applies
})

function checkOfferingEligibility(data: OfferingData, today: Date): {
  offerPermitted: boolean          // Art. 4(1)(a): legal persons only
  whitepaperRequired: boolean      // Art. 4(1)(b)–(d) unless Art. 4(2) or 4(3)
  exemptionBasis: string | null    // e.g. "Art. 4(2)(b)"
  earliestPublicationDate?: string // notified today + 20 working days (Art. 8(5))
  limitedNetworkNotificationRequired: boolean // Art. 4(3), third subparagraph
  violations: string[]
}
Art. 6

Content and Form of the Crypto-Asset White Paper (+ Annex I)

The white paper must contain the information listed in Art. 6(1) and Annex I: offeror, issuer and trading platform operator, the project, the offer, the crypto-asset, rights and obligations, the underlying technology, risks, and principal adverse impacts on the climate and the environment.

Obligations
  • Offeror / person seeking admission to trading, issuer and trading platform operator information (Art. 6(1)(a)–(c); Annex I, Parts A–C)
  • Information about the crypto-asset project (Art. 6(1)(d); Annex I, Part D)
  • Information about the offer or admission to trading (Art. 6(1)(e); Annex I, Part E)
  • Information about the crypto-asset, rights and obligations, underlying technology (Art. 6(1)(f)–(h); Annex I, Parts F–H)
  • Risks (Art. 6(1)(i); Annex I, Part I)
  • Principal adverse impacts on the climate and the environment of the consensus mechanism (Art. 6(1)(j))
  • Mandatory statements, including that the white paper has not been approved by a competent authority (Art. 6(3)–(6))
  • Summary in brief, non-technical language (Art. 6(7))
Art. 6 — TypeScript / ZodTypeScript
import { z } from "zod"

const LEI_REGEX = /^[A-Z0-9]{18}[0-9]{2}$/

const IssuerSchema = z.object({
  legalName:        z.string().min(1),
  legalForm:        z.string().min(1),
  lei:              z.string().regex(LEI_REGEX).optional(),
  registeredAddress: z.object({
    street:  z.string(),
    city:    z.string(),
    country: z.string().length(2),
    postcode: z.string(),
  }),
  ncaNotified:      z.string().length(2),  // MS where NCA notified
  managementBody:   z.array(z.object({
    name:  z.string(),
    role:  z.string(),
    dob:   z.string().date().optional(),
  })).min(1),
  websiteUrl:       z.string().url(),
  financialStatements: z.string().url().optional(),
})

const ConsensusMechanismEnum = z.enum([
  "PoS", "PoW", "dPoS", "PoA", "DAG", "other"
])

const WhitePaperSchema = z.object({
  // Metadata
  version:         z.string().default("1.0"),
  language:        z.string().default("en"),
  publicationDate: z.string().datetime().optional(),
  documentHash:    z.string().length(64).optional(), // SHA-256 hex

  // Art. 6(1)(a)–(b); Annex I, Parts A–B
  offeror:  IssuerSchema,
  issuer:   IssuerSchema.optional(), // if different from the offeror

  // Art. 6(1)(d); Annex I, Part D — Project
  reasonForOffer:     z.string().min(50),
  useOfProceeds:      z.string().min(50),
  technologyDescription: z.string().min(100),

  // Art. 6(1)(f)–(h); Annex I, Parts F–H — Crypto-asset, rights, technology
  tokenName:          z.string().min(1),
  tokenSymbol:        z.string().max(10),
  tokenType:          z.enum(["utility", "payment", "other_crypto_asset"]),
  totalSupply:        z.number().positive(),
  issuanceSchedule:   z.string(),
  distributionPlan:   z.array(z.object({
    recipient:    z.string(),
    amount:       z.number(),
    vestingMonths: z.number().int().nonnegative(),
    percentage:   z.number().min(0).max(100),
  })),
  holderRights:       z.array(z.string()).min(1),
  holderObligations:  z.array(z.string()),
  transferable:       z.boolean(),
  dltProtocol:        z.string(),
  smartContractAddress: z.string().optional(),
  smartContractAuditUrl: z.string().url().optional(),

  // Art. 6(1)(i); Annex I, Part I — Risks
  risks: z.object({
    offerRisks:      z.array(z.string()).min(1), // Annex I, Part I, point 1
    issuerRisks:     z.array(z.string()),        // point 2 (if issuer ≠ offeror)
    cryptoAssetRisks: z.array(z.string()).min(1), // point 3
    projectRisks:    z.array(z.string()).min(1), // point 4
    technologyRisks: z.array(z.string()).min(1), // point 5, with mitigation measures
  }),

  // Art. 6(1)(j) — Climate and environment-related adverse impacts
  consensusMechanism: ConsensusMechanismEnum,
  climateImpactDisclosure: z.string().optional(),
  annualEnergyKWh:    z.number().nonnegative().optional(),
  carbonFootprintTons: z.number().nonnegative().optional(),
})

// Compliance validator
function validateWhitePaper(wp: WhitePaper): {
  valid: boolean
  missingFields: string[]
  warnings: string[]
} {
  const missing: string[] = []
  const warnings: string[] = []

  // Art. 6(1)(j) applies whatever the consensus mechanism
  if (!wp.climateImpactDisclosure) {
    missing.push("Principal adverse impacts on the climate of the consensus mechanism (Art. 6(1)(j))")
  }
  if (!wp.smartContractAuditUrl) {
    warnings.push("Smart contract audit URL recommended")
  }
  if (wp.distributionPlan.reduce((s, d) => s + d.percentage, 0) !== 100) {
    missing.push("Distribution plan percentages must sum to 100%")
  }
  return { valid: missing.length === 0, missingFields: missing, warnings }
}
Art. 13

Right of Withdrawal (Retail Holders)

View POC

Retail holders buying directly from the offeror, or from a CASP placing on its behalf, may withdraw within 14 calendar days from their agreement to purchase; all payments are reimbursed within 14 days of the withdrawal notice.

Obligations
  • Right to withdraw within 14 CALENDAR DAYS from the date of the agreement to purchase (Art. 13(1))
  • No fees, no costs, no reasons required (Art. 13(1))
  • Reimburse all payments, including charges, within 14 days of being informed (Art. 13(2))
  • Information on the right of withdrawal in the white paper (Art. 13(3))
  • Not applicable once the crypto-assets were admitted to trading before purchase (Art. 13(4))
Key Thresholds

Withdrawal period: 14 CALENDAR DAYS from the purchase agreement

Refund deadline: 14 DAYS from withdrawal notice

Art. 13 — TypeScript / ZodTypeScript
import { z } from "zod"
import { addDays } from "date-fns"

const PurchaseSchema = z.object({
  purchaseId:      z.string().uuid(),
  purchaserId:     z.string(),
  offeringId:      z.string(),
  purchaseDate:    z.string().datetime(),
  whitepaperPublicationDate: z.string().datetime(),
  amountEUR:       z.number().positive(),
  tokenAmount:     z.number().positive(),
  transactionCostEUR: z.number().nonnegative(),
  isRetailPurchaser: z.boolean(),
  admittedToTradingBeforePurchase: z.boolean(), // Art. 13(4) exclusion
})

function getWithdrawalDeadline(purchase: Purchase): Date {
  // Art. 13(1): 14 calendar days from the agreement to purchase
  return addDays(new Date(purchase.purchaseDate), 14)
}

function getRefundDeadline(withdrawalNoticeDate: Date): Date {
  // 14 calendar days from date offeror is informed (Art. 13(2))
  return addDays(withdrawalNoticeDate, 14)
}

const WithdrawalSchema = z.object({
  withdrawalId:    z.string().uuid(),
  purchaseId:      z.string().uuid(),
  noticeDate:      z.string().datetime(),
  deadline:        z.string().datetime(),
  refundDue:       z.number(), // amountEUR + transactionCostEUR
  refundDeadline:  z.string().datetime(),
  status:          z.enum(["open", "withdrawn", "deadline_passed", "refunded"]),
})

Title III — Asset-Referenced Tokens (ART) — Art. 16–47

Art. 35

Own Funds Requirements for ART Issuers

View POC

ART issuers must at all times hold own funds equal to at least the highest of EUR 350,000, 2% of the average reserve of assets, or a quarter of the preceding year's fixed overheads. Significant ARTs use 3% (Art. 45(5)).

Obligations
  • Own funds ≥ MAX(EUR 350,000, 2% of average reserve, ¼ of fixed overheads) (Art. 35(1))
  • For Significant ART (Art. 45(5)): 3% instead of 2%
  • Own funds = Common Equity Tier 1 items and instruments (Art. 35(2))
  • Average reserve = end-of-day amounts over the preceding six months (Art. 35(1), second subparagraph)
  • Competent authority may require up to 20% more than the Art. 35(1)(b) amount (Art. 35(3))
Key Thresholds

Minimum floor: EUR 350,000

Standard percentage: 2% of average reserve (6 months)

Significant ART percentage: 3% of average reserve

Fixed overheads limb (Art. 35(1)(c)): ¼ of the preceding year's fixed overheads

Art. 35 — TypeScript / ZodTypeScript
import { z } from "zod"

// Daily reserve snapshot for 6-month rolling average
const ReserveSnapshotSchema = z.object({
  date:           z.string().date(),
  totalReserveEUR: z.number().nonnegative(),
})

function calculateRequiredOwnFunds(
  dailySnapshots: ReserveSnapshot[],       // end of each calendar day, preceding 6 months
  isSignificant: boolean,
  fixedOverheadsPrecedingYearEUR: number
): {
  averageReserve: number
  percentageRequirement: number    // 2% (Art. 35(1)(b)) or 3% (Art. 45(5))
  calculatedRequirement: number    // avg * pct
  overheadRequirement: number      // ¼ of fixed overheads (Art. 35(1)(c))
  requiredOwnFunds: number         // highest of the three limbs
  breachThreshold: number          // POC early warning at 110% (not a MiCA value)
} {
  const avg = dailySnapshots.reduce((s, d) => s + d.totalReserveEUR, 0)
              / dailySnapshots.length
  const pct = isSignificant ? 0.03 : 0.02
  const calculated = avg * pct
  const overheads = fixedOverheadsPrecedingYearEUR / 4
  // Art. 35(1): at least the highest of (a) EUR 350,000, (b) and (c)
  const required = Math.max(350_000, calculated, overheads)
  return {
    averageReserve: avg,
    percentageRequirement: pct,
    calculatedRequirement: calculated,
    overheadRequirement: overheads,
    requiredOwnFunds: required,
    breachThreshold: required * 1.1,
  }
}
Art. 36–38

Reserve of Assets — Composition, Custody and Investment

View POC

ART issuers must maintain a legally and operationally segregated reserve of assets, valued at market prices and at least equal to holders' claims, held in custody and invested only in highly liquid instruments. Minimum deposit levels are set by EBA RTS within floors fixed by the Regulation.

Obligations
  • Constitute and maintain a reserve covering the referenced assets and redemption liquidity risks (Art. 36(1))
  • Legal and operational segregation from the issuer's estate (Art. 36(2)–(3))
  • Deposits in credit institutions ≥ 30% of the amount referenced in each official currency (Art. 36(4)(d)); ≥ 60% for significant ARTs (Art. 45(7)(b))
  • Aggregate reserve value ≥ claims of holders, using market prices (Art. 36(7))
  • Custody by a CASP, credit institution or investment firm within 5 working days of issuance (Art. 37(3))
  • Avoid concentration of custodians and of reserve assets (Art. 37(1)(d)–(e))
  • Invest only in highly liquid financial instruments with minimal market, credit and concentration risk (Art. 38(1))
  • Independent audit every six months (Art. 36(9)); publish reserve composition at least monthly (Art. 30(1))
Key Thresholds

Deposit minimum (Art. 36(4)(d)): ≥ 30% per referenced currency

Deposit minimum, significant ART (Art. 45(7)(b)): ≥ 60% per referenced currency

Custody deadline (Art. 37(3)): 5 WORKING DAYS after issuance

Independent audit (Art. 36(9)): Every 6 months

Art. 36–38 — TypeScript / ZodTypeScript
import { z } from "zod"

// Asset categories modelled by the POC. Art. 38(1) only allows highly liquid
// financial instruments specified by EBA RTS; the list below is a POC assumption.
const AssetCategoryEnum = z.enum([
  "deposit_credit_institution",  // counts towards Art. 36(4)(d) / 45(7)(b)
  "ucits_money_market_fund",
  "eu_government_bond",
  "central_bank_deposit",        // not a deposit in a credit institution
])

const ReserveAssetSchema = z.object({
  assetId:      z.string().uuid(),
  category:     AssetCategoryEnum,
  institution:  z.string(),
  currency:     z.string().length(3),  // ISO 4217
  marketValueEUR: z.number().nonnegative(), // market prices (Art. 36(7))
  custodian:    z.string(),            // Art. 37(3): CASP, credit institution or investment firm
  verifiedAt:   z.string().datetime(),
})

const ReserveStateSchema = z.object({
  snapshotDate:        z.string().datetime(),
  referenceCurrency:   z.string().length(3),
  holderClaimsEUR:     z.number().nonnegative(), // aggregate claims of holders (Art. 36(7))
  assets:              z.array(ReserveAssetSchema),
})

function validateReserveCompliance(state: ReserveState, isSignificant: boolean) {
  const total = state.assets.reduce((s, a) => s + a.marketValueEUR, 0)
  const deposits = state.assets
    .filter(a => a.category === "deposit_credit_institution" && a.currency === state.referenceCurrency)
    .reduce((s, a) => s + a.marketValueEUR, 0)
  // Floors: ≥ 30% (Art. 36(4)(d)), ≥ 60% for significant ARTs (Art. 45(7)(b));
  // the exact amounts are set by EBA RTS within these floors
  const minDeposits = isSignificant ? 0.60 : 0.30

  // Art. 37(1)(d)–(e): avoid concentration; 10% is an illustrative POC policy limit
  const byInstitution: Record<string, number> = {}
  state.assets.forEach(a => {
    byInstitution[a.institution] = (byInstitution[a.institution] || 0) + a.marketValueEUR
  })
  const maxConcentration = Math.max(...Object.values(byInstitution)) / total

  return {
    deposits_min:  { pass: deposits / total >= minDeposits, actual: deposits / total, required: minDeposits },
    concentration: { pass: maxConcentration <= 0.10, maxSingleInstitution: maxConcentration },
    // Art. 36(7): aggregate value at market prices ≥ aggregate claims of holders
    full_coverage: { pass: total >= state.holderClaimsEUR, coverageRatio: total / state.holderClaimsEUR },
  }
}
Art. 39–40

ART Right of Redemption & Prohibition of Interest

View POC

Holders have a right of redemption at all times, paid in funds equal to the market value of the referenced assets or by delivering those assets, under a redemption policy set by the issuer. Redemption is free of charge and no interest may be granted.

Obligations
  • Right of redemption at ALL TIMES against the issuer (Art. 39(1))
  • Redeem in funds at the market value of the referenced assets, or by delivering the assets (Art. 39(2))
  • Redemption policy sets conditions, periods and timeframes (Art. 39(2)(a)) — MiCA fixes no deadline
  • Redemption shall not be subject to a fee (Art. 39(3))
  • Issuers and CASPs shall not grant interest, including time-based benefits (Art. 40)
Key Thresholds

Redemption timeframe: Set by the issuer's redemption policy (Art. 39(2)(a))

Redemption fee: None (Art. 39(3))

Interest/yield: PROHIBITED (Art. 40)

Art. 39–40 — TypeScript / ZodTypeScript
import { z } from "zod"
import { addBusinessDays } from "date-fns"

const ARTRedemptionSchema = z.object({
  requestId:       z.string().uuid(),
  holderId:        z.string(),
  tokenAmount:     z.number().positive(),
  requestedAt:     z.string().datetime(),
  mode:            z.enum(["reference_assets", "fiat_equivalent"]),
  destinationAccount: z.string(),
  marketValueEUR:  z.number().optional(),
  feeEUR:          z.number().nonnegative(), // Art. 39(3): 0, except a recovery-plan
                                             // liquidity fee under Art. 46(1)(a)
  netAmountEUR:    z.number().optional(),
  workingDayDeadline: z.string().datetime(),
  status:          z.enum([
    "pending",
    "processing",
    "completed",
    "rejected",
    "deadline_breached"
  ]),
})

function getARTRedemptionDeadline(requestDate: Date, policyWorkingDays: number): Date {
  // Timeframe defined in the issuer's redemption policy (Art. 39(2)(a))
  return addBusinessDays(requestDate, policyWorkingDays)
}

// Art. 40(3): any remuneration or benefit related to the holding period is interest,
// including net compensation or discounts with an equivalent effect
const InterestProhibitionSchema = z.object({
  tokenId:              z.string(),
  holderAddress:        z.string(),
  interestPaid:         z.literal(0),
  holdingPeriodBenefits: z.literal(0),
  equivalentDiscounts:  z.literal(0),
})

Title IV — E-Money Tokens (EMT) — Art. 48–58

Art. 49

Issuance and Redeemability of EMTs

View POC

EMTs are issued at par value on receipt of funds, and must be redeemed at any time and at par value, in funds other than e-money, upon request of the holder. Redemption is free of charge.

Obligations
  • Holders have a claim against the issuer (Art. 49(2))
  • Issue at PAR VALUE on receipt of funds (Art. 49(3))
  • Redeem at ANY TIME and at PAR VALUE, paid in funds other than e-money (Art. 49(4))
  • State the redemption conditions prominently in the white paper (Art. 49(5))
  • Redemption shall not be subject to a fee (Art. 49(6))
  • At least 30% of funds received deposited in separate accounts in credit institutions (Art. 54(a))
Key Thresholds

Redemption timing: At any time (Art. 49(4)) — MiCA fixes no processing deadline

Exchange rate: Par value (1:1)

Fees: None (Art. 49(6))

Deposits in credit institutions (Art. 54(a)): ≥ 30% of funds received

Art. 49 — TypeScript / ZodTypeScript
import { z } from "zod"
import { addBusinessDays, isAfter } from "date-fns"

const EMTRedemptionSchema = z.object({
  requestId:       z.string().uuid(),
  holderId:        z.string(),
  tokenAmount:     z.number().positive(),
  referenceCurrency: z.string().length(3),
  requestedAt:     z.string().datetime(),
  // PAR VALUE: 1 token = 1 unit of fiat
  parValuePerToken: z.literal(1),
  totalFiatAmount: z.number(), // = tokenAmount * 1
  feeEUR:          z.number().min(0).max(0), // Must be 0
  netAmount:       z.number(), // = totalFiatAmount (no deduction)
  destinationIBAN: z.string().regex(/^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$/),
  deadline:        z.string().datetime(), // issuer SLA (MiCA: "at any time")
  status:          z.enum([
    "pending",
    "processing",
    "completed",
    "sla_breached"
  ]),
})

function createEMTRedemption(
  tokenAmount: number,
  currency: string,
  requestDate: Date
): EMTRedemption {
  const deadline = addBusinessDays(requestDate, 1) // issuer policy, not a MiCA figure
  return {
    requestId: crypto.randomUUID(),
    tokenAmount,
    referenceCurrency: currency,
    parValuePerToken: 1,
    totalFiatAmount: tokenAmount * 1, // Par value
    feeEUR: 0,
    netAmount: tokenAmount, // Full amount, no fees
    deadline: deadline.toISOString(),
    status: "pending",
  }
}

function checkSLABreach(redemption: EMTRedemption): boolean {
  return redemption.status === "pending" && 
         isAfter(new Date(), new Date(redemption.deadline))
}
Art. 50

Prohibition of Granting Interest

View POC

Neither EMT issuers nor CASPs providing services related to EMTs may grant interest. Any remuneration or benefit related to the holding period counts as interest.

Obligations
  • PROHIBITED: Issuers granting interest on EMTs (Art. 50(1))
  • PROHIBITED: CASPs granting interest when providing services related to EMTs (Art. 50(2))
  • Any remuneration or benefit tied to the holding period is treated as interest (Art. 50(3))
  • Includes net compensation or discounts with an equivalent effect, from the issuer or third parties (Art. 50(3))
Key Thresholds

Interest/yield: PROHIBITED

Art. 50 — TypeScript / ZodTypeScript
import { z } from "zod"

// Art. 50(3): any remuneration or other benefit related to the length of time
// a holder holds the EMT is interest (incl. equivalent net compensation or discounts)
const EMTYieldComplianceSchema = z.object({
  tokenId:         z.string(),
  periodStart:     z.string().date(),
  periodEnd:       z.string().date(),
  interestPaid:           z.literal(0), // Art. 50(1) issuer, Art. 50(2) CASP
  holdingPeriodRewards:   z.literal(0), // Art. 50(3)
  equivalentCompensation: z.literal(0), // Art. 50(3): from the issuer or third parties
})

// Compliance check function
function validateArt50Compliance(
  tokenId: string,
  transactions: Transaction[]
): {
  compliant: boolean
  violations: string[]
} {
  const violations: string[] = []
  
  // Payments to holders computed from how long they held the EMT
  const yieldTransactions = transactions.filter(tx =>
    tx.type === "interest" ||
    (tx.type === "reward" && tx.basedOnHoldingPeriod) ||
    (tx.type === "discount" && tx.basedOnHoldingPeriod)
  )

  if (yieldTransactions.length > 0) {
    violations.push(
      `Art. 50 violation: ${yieldTransactions.length} yield-bearing transactions detected`
    )
  }

  return {
    compliant: violations.length === 0,
    violations,
  }
}

Title V — Crypto-Asset Service Providers (CASP) — Art. 59–85

Art. 67

Prudential Requirements for CASPs

View POC

CASPs must at all times hold prudential safeguards of at least the higher of the Annex IV permanent minimum capital for their services or a quarter of the preceding year's fixed overheads — as own funds, an insurance policy, or both.

Obligations
  • Prudential safeguards ≥ MAX(Annex IV minimum, ¼ × fixed overheads of the preceding year) (Art. 67(1))
  • Class 1 (EUR 50,000): advice, portfolio management, reception/transmission, execution, placing, transfer
  • Class 2 (EUR 125,000): class 1 services + custody, exchange for funds or other crypto-assets
  • Class 3 (EUR 150,000): class 2 services + operation of a trading platform
  • Form: CET1 own funds, an insurance policy, or a combination (Art. 67(4))
Key Thresholds

Class 1 minimum (Annex IV): EUR 50,000

Class 2 minimum (Annex IV): EUR 125,000

Class 3 minimum (Annex IV): EUR 150,000

Alternative calculation (Art. 67(1)(b)): ¼ of prior year fixed overheads

Art. 67 — TypeScript / ZodTypeScript
import { z } from "zod"

const CASPServiceEnum = z.enum([
  // Class 1 — EUR 50,000 (Annex IV)
  "advice",
  "portfolio_management",
  "reception_transmission",
  "execution",
  "placing",
  "transfer",
  // Class 2 — EUR 125,000
  "exchange_fiat",
  "exchange_crypto",
  "custody",
  // Class 3 — EUR 150,000
  "trading_platform",
])

// Annex IV — permanent minimum capital (Art. 67(1)(a))
const TIER_MINIMUMS = {
  tier1: 50_000,
  tier2: 125_000,
  tier3: 150_000,
} as const

const SERVICE_TIERS: Record<z.infer<typeof CASPServiceEnum>, keyof typeof TIER_MINIMUMS> = {
  advice: "tier1",
  reception_transmission: "tier1",
  execution: "tier1",
  placing: "tier1",
  transfer: "tier1",
  portfolio_management: "tier1",
  exchange_fiat: "tier2",
  exchange_crypto: "tier2",
  custody: "tier2",
  trading_platform: "tier3",
}

function calculateCASPOwnFunds(
  services: z.infer<typeof CASPServiceEnum>[],
  annualFixedOverhead: number
): {
  highestTier: keyof typeof TIER_MINIMUMS
  tierMinimum: number
  quarterOverhead: number
  requiredOwnFunds: number
} {
  // Determine highest tier from selected services
  const tiers = services.map(s => SERVICE_TIERS[s])
  const highestTier = tiers.includes("tier3") ? "tier3"
    : tiers.includes("tier2") ? "tier2" : "tier1"

  const tierMinimum = TIER_MINIMUMS[highestTier]
  const quarterOverhead = annualFixedOverhead / 4 // Art. 67(1)(b)

  return {
    highestTier,
    tierMinimum,
    quarterOverhead,
    requiredOwnFunds: Math.max(tierMinimum, quarterOverhead),
  }
}
Art. 71

Complaints-Handling Procedures

View POC

CASPs must run effective, transparent and free complaint procedures, keep a record of all complaints, and investigate them in a timely and fair manner. MiCA itself sets no fixed deadline; the RTS adopted under Art. 71(5) cap it at 2 months from receipt of the complaint (Art. 6(2) of Delegated Regulation (EU) 2025/294).

Obligations
  • Establish and publish effective, transparent complaint procedures (Art. 71(1))
  • FREE OF CHARGE to clients (Art. 71(2))
  • Inform clients, provide a complaint template, keep a record of all complaints and measures taken (Art. 71(3))
  • Investigate in a timely and fair manner; communicate the outcome within a reasonable period (Art. 71(4))
  • Communicate the decision within 2 MONTHS of receiving the complaint (Art. 6(2) of Delegated Regulation (EU) 2025/294)
  • In exceptional situations where that is not possible, tell the client the reasons for the delay and the date of the decision (Art. 6(3) of Delegated Regulation (EU) 2025/294)
  • Keep records of services and activities for 5 YEARS (Art. 68(9))
Key Thresholds

Response time: "Reasonable period" (Art. 71(4)); no fixed deadline in MiCA itself

Decision limit (complaints RTS): 2 MONTHS from receipt (Art. 6(2) of Delegated Regulation (EU) 2025/294)

Illustrative policy SLA (POC): 5 / 15 / 35 BUSINESS DAYS

Record retention (Art. 68(9)): 5 YEARS

Art. 71 — TypeScript / ZodTypeScript
import { z } from "zod"
import { addBusinessDays, addMonths, differenceInBusinessDays, isAfter } from "date-fns"

const ComplaintSchema = z.object({
  complaintId:      z.string().uuid(),
  clientId:         z.string(),
  submittedAt:      z.string().datetime(),
  subject:          z.string().min(10),
  description:      z.string().min(50),
  // SLA tracking (internal policy — MiCA requires "timely" handling)
  ackDeadline:      z.string().datetime(), // +5 business days
  ackSentAt:        z.string().datetime().optional(),
  responseDeadline: z.string().datetime(), // +15 business days
  extendedDeadline: z.string().datetime().optional(), // +35 if extended, capped at decisionDeadline
  // Regulatory cap: decision within 2 months of receipt (RTS 2025/294)
  decisionDeadline: z.string().datetime(),
  extensionReason:  z.string().optional(),
  responseSentAt:   z.string().datetime().optional(),
  // Status
  status: z.enum([
    "received",
    "acknowledged",
    "under_review",
    "extended",
    "resolved",
    "sla_breached"
  ]),
  // Retention: 5 years (Art. 68(9))
  retentionUntil:   z.string().datetime().optional(),
})

function createComplaint(clientId: string, subject: string, description: string): Complaint {
  const now = new Date()
  return {
    complaintId: crypto.randomUUID(),
    clientId,
    submittedAt: now.toISOString(),
    subject,
    description,
    ackDeadline: addBusinessDays(now, 5).toISOString(),
    responseDeadline: addBusinessDays(now, 15).toISOString(),
    decisionDeadline: addMonths(now, 2).toISOString(),
    status: "received",
  }
}

function checkSLAStatus(complaint: Complaint): {
  ackOverdue: boolean
  responseOverdue: boolean
  decisionOverdue: boolean // inform the client of the reasons and the decision date
  daysUntilAck: number
  daysUntilResponse: number
} {
  const now = new Date()
  const ackDeadline = new Date(complaint.ackDeadline)
  const responseDeadline = new Date(
    complaint.extendedDeadline || complaint.responseDeadline
  )

  return {
    ackOverdue: !complaint.ackSentAt && isAfter(now, ackDeadline),
    responseOverdue: !complaint.responseSentAt && isAfter(now, responseDeadline),
    decisionOverdue:
      !complaint.responseSentAt && isAfter(now, new Date(complaint.decisionDeadline)),
    daysUntilAck: differenceInBusinessDays(ackDeadline, now),
    daysUntilResponse: differenceInBusinessDays(responseDeadline, now),
  }
}
Art. 81

Suitability Assessment (Advice & Portfolio Management)

View POC

CASPs providing advice or portfolio management must assess suitability based on the client's knowledge and experience, investment objectives including risk tolerance, and financial situation including the ability to bear losses.

Obligations
  • Obtain the necessary information before the service (Art. 81(1), (8)):
  • (a) Knowledge of and experience in investing, including in crypto-assets
  • (b) Investment objectives, including risk tolerance
  • (c) Financial situation, including the ability to bear losses
  • Do not recommend or manage where the service is unsuitable (Art. 81(11))
  • Review each client's suitability at least every two years (Art. 81(12))
  • Provide a suitability report in electronic format for advice (Art. 81(13))
Art. 81 — TypeScript / ZodTypeScript
import { z } from "zod"

const KnowledgeAssessmentSchema = z.object({
  yearsOfExperience: z.number().int().min(0),
  cryptoTypesTraded: z.array(z.enum([
    "bitcoin", "ethereum", "stablecoins", "defi", "nfts", "other"
  ])),
  understandsVolatility: z.boolean(),
  understandsPrivateKeys: z.boolean(),
  previousLosses: z.boolean(),
  selfAssessedLevel: z.enum(["novice", "intermediate", "advanced"]),
})

const FinancialSituationSchema = z.object({
  annualIncomeEUR: z.number().nonnegative(),
  liquidAssetsEUR: z.number().nonnegative(),
  totalLiabilitiesEUR: z.number().nonnegative(),
  monthlyExpensesEUR: z.number().nonnegative(),
  emergencyFundMonths: z.number().int().min(0),
  dependents: z.number().int().min(0),
})

const InvestmentObjectivesSchema = z.object({
  purpose: z.enum([
    "capital_growth",
    "income_generation",
    "speculation",
    "hedging",
    "diversification"
  ]),
  timeHorizon: z.enum([
    "less_than_1_year",
    "1_to_3_years",
    "3_to_5_years",
    "more_than_5_years"
  ]),
  riskTolerance: z.enum(["low", "medium", "high"]),
  maxPortfolioAllocation: z.number().min(0).max(100),
})

const LossCapacitySchema = z.object({
  maxAcceptableLossPct: z.number().min(0).max(100),
  canAffordTotalLoss: z.boolean(),
  impactOnLifestyle: z.enum(["none", "minor", "significant", "severe"]),
})

const SuitabilityAssessmentSchema = z.object({
  assessmentId:    z.string().uuid(),
  clientId:        z.string(),
  assessedAt:      z.string().datetime(),
  knowledge:       KnowledgeAssessmentSchema,
  financialSituation: FinancialSituationSchema,
  objectives:      InvestmentObjectivesSchema,
  lossCapacity:    LossCapacitySchema,
  suitabilityScore: z.number().min(0).max(100),
  recommendation:  z.enum(["suitable", "potentially_suitable", "not_suitable"]),
  warnings:        z.array(z.string()),
  nextReviewDue:   z.string().datetime(), // At least every two years (Art. 81(12))
})

function calculateSuitabilityScore(assessment: SuitabilityAssessment): number {
  let score = 50 // Base score

  // Knowledge adjustments
  if (assessment.knowledge.yearsOfExperience >= 3) score += 15
  if (assessment.knowledge.understandsVolatility) score += 10

  // Financial health adjustments
  const netWorth = assessment.financialSituation.liquidAssetsEUR 
    - assessment.financialSituation.totalLiabilitiesEUR
  if (netWorth > 100_000) score += 10
  if (assessment.financialSituation.emergencyFundMonths >= 6) score += 10

  // Risk tolerance alignment
  if (assessment.objectives.riskTolerance === "low") score -= 15
  if (assessment.lossCapacity.canAffordTotalLoss) score += 10

  return Math.max(0, Math.min(100, score))
}

Title VI — Market Abuse Prevention — Art. 86–92

Art. 89

Prohibition of Insider Dealing

View POC

No person may use inside information to acquire or dispose of crypto-assets, or recommend or induce others to do so. Inside information is defined in Art. 87; unlawful disclosure is prohibited by Art. 90.

Obligations
  • PROHIBITED: Acquiring/disposing crypto-assets using inside information (Art. 89)
  • PROHIBITED: Recommending/inducing others to trade based on inside information (Art. 89)
  • PROHIBITED: Unlawful disclosure of inside information outside the normal exercise of duties (Art. 90)
  • Inside information = precise, non-public, likely to have a significant effect on prices (Art. 87)
  • Issuers must disclose inside information to the public as soon as possible (Art. 88)
Art. 89 — TypeScript / ZodTypeScript
import { z } from "zod"

const InsideInformationSchema = z.object({
  informationId:   z.string().uuid(),
  cryptoAssetId:   z.string(),
  description:     z.string().min(20),
  // Criteria for "inside information"
  isPrecise:       z.boolean(),     // Specific enough to conclude price effect
  isNonPublic:     z.boolean(),     // Not yet disclosed to market
  isPriceSensitive: z.boolean(),    // Would likely affect price if disclosed
  discoveredAt:    z.string().datetime(),
  disclosedAt:     z.string().datetime().optional(),
  source:          z.enum([
    "issuer_employee",
    "casp_employee",
    "advisor",
    "third_party",
    "technical_discovery"
  ]),
})

const InsiderTradeCheckSchema = z.object({
  tradeId:         z.string().uuid(),
  traderId:        z.string(),
  cryptoAssetId:   z.string(),
  tradeTimestamp:  z.string().datetime(),
  direction:       z.enum(["buy", "sell"]),
  amount:          z.number().positive(),
  // Compliance fields
  hadInsideInfo:   z.boolean(),
  insideInfoIds:   z.array(z.string().uuid()),
  complianceCheck: z.enum([
    "clear",           // No inside info
    "flagged",         // Potential violation
    "confirmed_breach" // Verified Art. 89 breach
  ]),
})

function checkInsiderTrade(
  trade: Trade,
  activeInsideInfo: InsideInformation[]
): {
  violation: boolean
  matchingInfo: InsideInformation[]
  recommendedAction: string
} {
  const matching = activeInsideInfo.filter(info =>
    info.cryptoAssetId === trade.cryptoAssetId &&
    info.isPrecise && info.isNonPublic && info.isPriceSensitive &&
    !info.disclosedAt // Still non-public
  )

  return {
    violation: matching.length > 0,
    matchingInfo: matching,
    recommendedAction: matching.length > 0
      ? "HALT_TRADE_AND_REPORT_TO_NCA"
      : "PROCEED",
  }
}
Art. 91

Prohibition of Market Manipulation

View POC

No person may engage in or attempt market manipulation: false or misleading signals, securing prices at artificial levels, deceptive devices, or disseminating false information (Art. 91(2)–(3)).

Obligations
  • PROHIBITED: Transactions or orders giving false or misleading signals, e.g. wash trading (Art. 91(2)(a))
  • PROHIBITED: Securing prices at an abnormal or artificial level (Art. 91(2)(a))
  • PROHIBITED: Fictitious devices or other forms of deception or contrivance (Art. 91(2)(b))
  • PROHIBITED: Placing, cancelling or modifying orders to create false signals, e.g. layering/spoofing (Art. 91(3)(b))
  • PROHIBITED: Disseminating false or misleading information, including rumours (Art. 91(2)(c))
  • Persons professionally arranging or executing transactions must detect and report suspicions without delay (Art. 92(1))
Art. 91 — TypeScript / ZodTypeScript
import { z } from "zod"

const ManipulationTypeEnum = z.enum([
  "wash_trading",
  "layering",
  "spoofing",
  "pump_and_dump",
  "coordinated_trading",
  "false_information",
])

const SuspiciousPatternSchema = z.object({
  patternId:       z.string().uuid(),
  detectedAt:      z.string().datetime(),
  cryptoAssetId:   z.string(),
  manipulationType: ManipulationTypeEnum,
  confidence:      z.number().min(0).max(1),
  indicators:      z.array(z.string()),
  involvedAddresses: z.array(z.string()),
  volumeAffected:  z.number().nonnegative(),
  priceImpactPct:  z.number(),
  status:          z.enum([
    "detected",
    "under_review",
    "confirmed",
    "dismissed",
    "reported_to_nca"
  ]),
})

// Wash trading detection
function detectWashTrading(
  trades: Trade[],
  timeWindowMinutes: number = 60
): SuspiciousPattern[] {
  const patterns: SuspiciousPattern[] = []
  
  // Group trades by address pairs
  const addressPairs = new Map<string, Trade[]>()
  trades.forEach(trade => {
    const key = [trade.fromAddress, trade.toAddress].sort().join("|")
    if (!addressPairs.has(key)) addressPairs.set(key, [])
    addressPairs.get(key)!.push(trade)
  })

  // Check for circular patterns
  addressPairs.forEach((pairTrades, key) => {
    if (pairTrades.length >= 3) {
      const buys = pairTrades.filter(t => t.direction === "buy")
      const sells = pairTrades.filter(t => t.direction === "sell")
      
      if (buys.length > 0 && sells.length > 0) {
        patterns.push({
          patternId: crypto.randomUUID(),
          detectedAt: new Date().toISOString(),
          cryptoAssetId: pairTrades[0].cryptoAssetId,
          manipulationType: "wash_trading",
          confidence: 0.8,
          indicators: [
            "Circular trading between same addresses",
            `${pairTrades.length} trades in ${timeWindowMinutes} minutes`,
          ],
          involvedAddresses: key.split("|"),
          volumeAffected: pairTrades.reduce((s, t) => s + t.amount, 0),
          priceImpactPct: 0,
          status: "detected",
        })
      }
    }
  })

  return patterns
}

Regulation (EU) 2023/1114 — Markets in Crypto-Assets Regulation (MiCA)

CELEX:32023R1114 · Records retained 5 years per Art. 75 · Fully applicable from: December 30, 2024