Skip to main content
Version: 0.15

Epoch on Miden

Epoch gives a Miden application an intent-based bridge surface: request a quote, let the user authorize the source collateral, submit the intent, and track the solver's destination transaction.

The current reference integration supports Epoch test USDC between Miden testnet and Ethereum Sepolia in both directions, with a typical testnet time of 1–3 minutes.

Why Epoch is faster

Epoch's allocator and solver coordinate the destination leg after the source collateral is authorized. The application does not wait for the canonical Agglayer exit and claim lifecycle, which produces a faster testnet experience.

The tradeoff is an additional provider boundary: quoting, observation, solver availability, and destination fulfillment depend on Epoch services. Treat the 1–3 minute range as an estimate, not an SLA.

Dependencies

npm install @epoch-protocol/epoch-intents-sdk \
@miden-sdk/miden-sdk \
@miden-sdk/miden-wallet-adapter-base \
@miden-sdk/miden-wallet-adapter-react \
viem

The snippets are type-checked against @epoch-protocol/[email protected], @miden-sdk/[email protected], Miden wallet adapter 0.15.1, and [email protected].

The current reference uses:

export const EPOCH_ALLOCATOR_URL =
"https://testnet-dev.epochprotocol.xyz";
export const MIDEN_CHAIN_ID = 999999999;
export const SEPOLIA_CHAIN_ID = 11155111;
export const RECLAIM_WINDOW_BLOCKS = 1000;

Initialize the SDK per direction

The wallet client chain ID tells Epoch which side supplies the collateral:

  • Use virtual chain ID 999999999 for Miden → EVM.
  • Keep the real Sepolia chain ID for EVM → Miden.
import { EpochIntentSDK } from "@epoch-protocol/epoch-intents-sdk";
import {
createWalletClient,
custom,
type Chain,
type EIP1193Provider,
} from "viem";
import { sepolia } from "viem/chains";

export function createEpochSdk(
account: `0x${string}`,
provider: EIP1193Provider,
source: "miden" | "sepolia",
) {
const chain: Chain =
source === "miden"
? { ...sepolia, id: MIDEN_CHAIN_ID }
: sepolia;
const walletClient = createWalletClient({
account,
chain,
transport: custom(provider),
});

return new EpochIntentSDK({
apiBaseUrl: EPOCH_ALLOCATOR_URL,
walletClient,
});
}

For Miden-source flows the EVM account acts as the intent sponsor; it does not supply the collateral. Do not reuse the virtual-chain override for EVM-source intents. The reverse direction signs against the connected Sepolia wallet and therefore needs chain ID 11155111.

Miden → EVM

1. Compute a reclaim height

The intent declares an absolute Miden block height, while the wallet adapter uses a relative recall window. Derive both from the same chain tip:

const currentBlock = await getCurrentMidenBlock();
const reclaimHeight = currentBlock + RECLAIM_WINDOW_BLOCKS;

Never hard-code 1000 as the absolute reclaim height. On a chain above that height, the note would be reclaimable as soon as it was created.

2. Build and quote the intent

import { TaskType } from "@epoch-protocol/epoch-intents-sdk";

const task = await sdk.getTaskData({
taskType: TaskType.GetTokenOut,
intentData: {
isNative: false,
depositTokenAddress: ZERO_ADDRESS,
tokenInAmount: midenAmountInBaseUnits,
outputTokenAddress: epochSepoliaUsdcAddress,
minTokenOut: minimumEvmOutput,
destinationChainId: String(SEPOLIA_CHAIN_ID),
protocolHashIdentifier: ZERO_HASH,
recipient: evmRecipient,
},
extraDataTypestring:
"string midenSourceAccount,string midenFaucetId,string midenNoteType,string midenNoteId,uint256 midenReclaimHeight",
extraData: {
midenSourceAccount,
midenFaucetId,
midenNoteType: "P2IDE",
midenNoteId: "",
midenReclaimHeight: String(reclaimHeight),
},
});

const quote = await sdk.getIntentQuote({
sponsorAddress: evmRecipient,
taskTypeString: task.taskTypeString,
intentData: task.intentData,
isNative: false,
});

Amounts in the intent envelope are base-unit strings. Do not apply display decimals twice.

3. Create the Miden collateral note

Epoch calls createMidenP2IDNote during solveIntent. The callback must create a public, reclaimable P2IDE note so the solver can observe it and the user can recover funds if the intent expires.

import {
AccountId,
AccountInterface,
NetworkId,
} from "@miden-sdk/miden-sdk";

function toTestnetAccountAddress(value: string) {
return value.startsWith("0x")
? AccountId.fromHex(value).toBech32(
NetworkId.testnet(),
AccountInterface.BasicWallet,
)
: value;
}

const createMidenP2IDNote = async (
faucetId: string,
amount: string,
allocatorId: string,
) => {
const amountBaseUnits = BigInt(amount);
if (amountBaseUnits > BigInt(Number.MAX_SAFE_INTEGER)) {
return { success: false };
}

const requestId = await requestSend({
senderAddress: midenSender,
recipientAddress: toTestnetAccountAddress(allocatorId),
faucetId: toTestnetAccountAddress(faucetId),
noteType: "public",
amount: Number(amountBaseUnits),
recallBlocks: RECLAIM_WINDOW_BLOCKS,
});
const output = await waitForTransaction(requestId);
const noteId = output.outputNotes?.[0]?.id().toString();

return noteId
? { success: true, noteId }
: { success: false };
};

The Number.MAX_SAFE_INTEGER guard must run after parsing the SDK's base-unit string and before converting it for the wallet adapter. Otherwise JavaScript can silently round the collateral amount and make it differ from the intent.

4. Solve and track the destination

import { CollateralType } from "@epoch-protocol/epoch-intents-sdk";

const result = await sdk.solveIntent({
isNative: false,
sponsorAddress: evmRecipient,
taskTypeString: task.taskTypeString,
intentData: task.intentData,
quoteResult: quote,
collateralType: CollateralType.Miden,
midenFaucetId,
midenSourceAccount,
createMidenP2IDNote,
});

Persist the sponsor address and intent nonce returned by the solve result. Poll getIntentStatus(sponsorAddress, nonce) and mark the transfer complete only after a successful Sepolia row has a destination transaction hash and no Sepolia row remains pending.

EVM → Miden

1. Build the reverse task

const task = await sdk.getTaskData({
taskType: TaskType.GetTokenOut,
intentData: {
isNative: false,
depositTokenAddress: epochSepoliaUsdcAddress,
tokenInAmount: evmAmountInBaseUnits,
outputTokenAddress: ZERO_ADDRESS,
minTokenOut: minimumMidenOutput,
destinationChainId: String(MIDEN_CHAIN_ID),
protocolHashIdentifier: ZERO_HASH,
recipient: evmSourceAddress,
},
extraDataTypestring:
"string midenRecipientAccount,string midenFaucetId,string midenNoteType",
extraData: {
midenRecipientAccount,
midenFaucetId,
midenNoteType: "P2ID",
},
});

Use P2ID, not P2IDE, for the destination note. This direction delivers to the Miden recipient rather than creating reclaimable Miden-side collateral.

2. Quote and solve with EVM collateral

const quote = await sdk.getIntentQuote({
sponsorAddress: evmSourceAddress,
taskTypeString: task.taskTypeString,
intentData: task.intentData,
isNative: false,
});

const result = await sdk.solveIntent({
isNative: false,
sponsorAddress: evmSourceAddress,
taskTypeString: task.taskTypeString,
intentData: task.intentData,
quoteResult: quote,
collateralType: CollateralType.EVM,
});

The SDK may first request an ERC-20 approval and then depositERC20AndRegister against Compact. Surface those as separate wallet phases so the interface does not appear frozen.

Poll the intent until the Miden destination row settles. An intermediate Compact or allocator success is not destination completion.

Recovery

Persist the source transaction hash, sponsor address, intent nonce, direction, and destination chain ID. The SDK also exposes recovery operations for failed or cancelled flows:

  • retryIntentSolve retries a transient solver failure.
  • disableForcedWithdrawal clears a pending forced-withdrawal state before reusing a Compact deposit.
  • withdrawToken reclaims an unfulfilled EVM-side deposit.
  • initateDepositWithdrawal starts forced withdrawal. The exported method name currently contains that spelling.

Do not hide these states behind a generic "try again" button. Show which resource is locked and which recovery transaction will be signed.

Frontend integration constraints

  • Await waitForTransaction before reading a Miden output note ID.
  • Keep P2IDE collateral public so the allocator can observe it.
  • Dynamically import Miden/Epoch execution code in SSR applications because the Miden SDK initializes WASM eagerly.
  • Do not enable COOP/COEP with the current wallet-popup and Miden gRPC-Web transport stack.
  • Use a destination-aware polling reducer; source settlement is not completion.

Continue with Epoch

Miden implementation references