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
| Article | Threshold / 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. 43 | Significant ART: ≥ 3 of 7 criteria |
| Art. 43 | Holders threshold: > 10,000,000 |
| Art. 43 | Value 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 IV | CASP Class 1: EUR 50,000 |
| Art. 67(1)(a), Annex IV | CASP Class 2: EUR 125,000 |
| Art. 67(1)(a), Annex IV | CASP 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
Offers to the Public of Crypto-Assets
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
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[]
}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))
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 }
}Right of Withdrawal (Retail Holders)
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
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
Own Funds Requirements for ART Issuers
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
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,
}
}Reserve of Assets — Composition, Custody and Investment
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
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 Right of Redemption & Prohibition of Interest
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)
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
Issuance and Redeemability of EMTs
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
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))
}Prohibition of Granting Interest
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
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
Prudential Requirements for CASPs
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
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),
}
}Complaints-Handling Procedures
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
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),
}
}Suitability Assessment (Advice & Portfolio Management)
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))
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
Prohibition of Insider Dealing
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)
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",
}
}Prohibition of Market Manipulation
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))
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
}