On October 8, 2026, ESMA published an opinion telling MiCA-authorised crypto service providers to stop offering services for non-MiCA-compliant stablecoins to EU clients, and to remediate existing exposure within three months. The outer deadline is January 8, 2027. USDT is the largest affected token.
The interesting part for engineers is the shape of the rule. It isn't "remove the asset." It's:
- Block actions that let EU clients acquire or increase holdings
- Allow, temporarily, actions that support an orderly exit: sell, exchange, transfer, withdraw
- Apply per jurisdiction, with dates set by each platform's regulator, inside an outer limit
If your asset availability is a single enabled: boolean, you can't express that. Here's a model that can.
Availability is per action, per jurisdiction, over time
type Action =
| "buy"
| "deposit"
| "swap_in" // receive this asset as the output of a swap
| "swap_out" // spend this asset as the input of a swap
| "sell"
| "withdraw"
| "custody";
type Mode = "full" | "exit_only" | "unavailable";
interface AvailabilityRule {
asset: string; // asset key, e.g. "tron:trc20:<contract>"
jurisdiction: string; // e.g. "EU"
mode: Mode;
effectiveFrom: string; // ISO date
}
const EXIT_ONLY_ALLOWED: ReadonlySet<Action> = new Set([
"sell",
"swap_out",
"withdraw",
"custody",
]);
exit_only is the important state. It lets users reduce exposure and blocks anything that increases it. Note that swap_in and swap_out are separate actions. Converting out of a restricted asset is an exit; converting into one isn't.
Resolve the active rule at request time
Rules change on dates, so resolve them when the request arrives rather than caching a flag:
const MODE_RANK: Record<Mode, number> = {
full: 0,
exit_only: 1,
unavailable: 2,
};
function activeMode(
rules: AvailabilityRule[],
asset: string,
jurisdiction: string,
now: Date = new Date(),
): Mode {
const applicable = rules
.filter(
(r) =>
r.asset === asset &&
r.jurisdiction === jurisdiction &&
new Date(r.effectiveFrom) <= now,
)
.sort((a, b) => b.effectiveFrom.localeCompare(a.effectiveFrom));
return applicable[0]?.mode ?? "full";
}
export function isAllowed(
rules: AvailabilityRule[],
asset: string,
jurisdiction: string,
action: Action,
now?: Date,
): boolean {
const mode = activeMode(rules, asset, jurisdiction, now);
if (mode === "full") return true;
if (mode === "unavailable") return false;
return EXIT_ONLY_ALLOWED.has(action);
}
With this, the ESMA transition is data, not a deploy:
const rules: AvailabilityRule[] = [
{
asset: "tron:trc20:usdt",
jurisdiction: "EU",
mode: "exit_only",
effectiveFrom: "2026-10-20",
},
{
asset: "tron:trc20:usdt",
jurisdiction: "EU",
mode: "unavailable",
effectiveFrom: "2027-01-08",
},
];
Things that bite in practice
A swap touches two assets. Check swap_out for the input and swap_in for the output. A USDT-to-USDC conversion is an allowed exit; a USDC-to-USDT conversion isn't.
Pending orders cross the boundary. An order created in full mode and settled after the exit_only date needs a defined outcome. Decide whether you honour, refund, or re-quote it, and store the decision with the order.
Mode changes must be visible. Show the restriction and the date in the UI before it takes effect. Silent failures on a regulatory date generate the worst kind of support load.
Never decrease rank silently. If two rules collide on the same date, prefer the more restrictive mode. MODE_RANK exists so you can enforce that in validation when rules are loaded.
Jurisdiction is an input, not a constant. It comes from your compliance layer, and it can change for a user. Pass it explicitly rather than reading a global.
Why this matters beyond the EU
The same week as ESMA's opinion, FinCEN in the US withdrew its unhosted-wallet and mixing proposals. Rules are diverging across jurisdictions, and they change on dates. Systems that model availability as time-bound, per-action, per-jurisdiction data can follow those changes without emergency code changes.
Disclosure: written by the NefiSwap team. NefiSwap supports conversion across 8,000+ assets via the web, a Telegram Mini App and bot, and an API. nefiswap.com
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.