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;
Initialize the SDK per direction
The wallet client chain ID tells Epoch which side supplies the collateral:
- Use virtual chain ID
999999999for 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. Use the allocator's reclaim window
Epoch SDK 1.0.39 supplies recallBlocks and bindingAttachmentFelts to the
collateral callback. Compute the absolute reclaim height from the chain head
when creating the note. Do not put a precomputed reclaim height in the intent.
The attachment binds the collateral to the intent and must be written unchanged.
2. Build and quote the intent
import { TaskType, MIDEN_TO_EVM_EXTRA_TYPESTRING, EVM_TO_MIDEN_EXTRA_TYPESTRING } 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: MIDEN_TO_EVM_EXTRA_TYPESTRING,
extraData: {
midenSourceAccount,
midenFaucetId,
midenNoteType: "P2IDE",
midenNoteId: "",
},
});
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 createMidenP2IDENote 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,
FungibleAsset, Note, NoteArray, NoteAssets, NoteAttachment, NoteType,
TransactionRequestBuilder, Word,
} from "@miden-sdk/miden-sdk";
import { Transaction } from "@miden-sdk/miden-wallet-adapter-base";
/** Fresh fee-conversion salt for custom requests, also used as a multisig replay guard. */
function createFeeConversionSalt(): Word {
const felts = new BigUint64Array(4);
for (let i = 0; i < felts.length; i++) {
// Word requires canonical Goldilocks field elements. Reject the tiny
// out-of-field range instead of rounding or reducing random values.
do {
crypto.getRandomValues(felts.subarray(i, i + 1));
} while (felts[i] >= 18_446_744_069_414_584_321n);
}
return new Word(felts);
}
function toTestnetAccountAddress(value: string) {
return value.startsWith("0x")
? AccountId.fromHex(value).toBech32(
NetworkId.testnet(),
AccountInterface.BasicWallet,
)
: value;
}
const createMidenP2IDENote = async (
faucetId: string,
amount: string,
allocatorId: string,
recallBlocks: number,
bindingAttachmentFelts: bigint[],
) => {
const currentBlock = await getCurrentMidenBlock();
const reclaimHeight = currentBlock + recallBlocks;
if (!Number.isSafeInteger(reclaimHeight) || recallBlocks <= 0 ||
reclaimHeight > 0xffff_ffff || bindingAttachmentFelts.length === 0) {
throw new Error("Invalid Epoch collateral parameters");
}
const toAccountId = (value: string) => value.startsWith("0x")
? AccountId.fromHex(value) : AccountId.fromBech32(value);
const note = Note.createP2IDENote(
toAccountId(midenSourceAccount),
toAccountId(allocatorId),
new NoteAssets([new FungibleAsset(toAccountId(faucetId), BigInt(amount))]),
reclaimHeight,
null,
NoteType.Public,
new NoteAttachment(BigUint64Array.from(bindingAttachmentFelts)),
);
const expectedNoteId = note.id().toString();
const request = new TransactionRequestBuilder()
.withFeeConversionSalt(createFeeConversionSalt())
.withOwnOutputNotes(new NoteArray([note]))
.build();
const requestId = await requestTransaction(Transaction.createCustomTransaction(
midenSender, toTestnetAccountAddress(allocatorId), request,
));
const output = await waitForTransaction(requestId);
const noteId = output.outputNotes?.find(
(outputNote) => outputNote.id().toString() === expectedNoteId,
)?.id().toString();
return noteId ? { success: true, noteId } : { success: false };
};
The custom transaction preserves the SDK attachment and keeps the amount as a
bigint. Match the collateral note by ID because a transaction can also emit a
fee note. requestSend cannot carry the required attachment.
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,
createMidenP2IDENote,
});
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: EVM_TO_MIDEN_EXTRA_TYPESTRING,
extraData: {
midenRecipientAccount,
midenFaucetId,
},
});
Use EVM_TO_MIDEN_EXTRA_TYPESTRING from the Epoch SDK for this direction.
The allocator delivers to the Miden recipient; no collateral callback is needed.
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:
retryIntentSolveretries a transient solver failure.disableForcedWithdrawalclears a pending forced-withdrawal state before reusing a Compact deposit.withdrawTokenreclaims an unfulfilled EVM-side deposit.initateDepositWithdrawalstarts 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
waitForTransactionbefore 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 integration example ↗
Review Epoch's current Miden task data, collateral model, and supported provider workflow.
Epoch SDK integration guide ↗
Check current SDK initialization, quoting, submission, status, and recovery APIs.
Miden implementation references
- Epoch integration in the bridge portal
- Full Epoch bridging tutorial
- Runnable bridging application
- Epoch SDK reference
- Supported chains and tokens
Earn in Bread
Bread's Earn flow uses Epoch intents to coordinate a lending interaction on an EVM-side configured market. Miden supplies collateral through a recallable P2IDE note; a solver fulfills the market interaction. This is not evidence of a lending contract executing natively on Miden or of a guaranteed return.
The bridge, quote/solve, P2IDE collateral and note-recovery flow above also supports this wallet position lifecycle. These additional boundaries do not define a new provider API contract.
Use the market and asset configuration supported by the current provider deployment. Public wallet code and test markets do not establish production market availability, a particular lending protocol, a fixed yield, or a zero-fee route.
Confirm the route before exposing Earn
Treat each market as a specific source network, collateral faucet, destination chain, market and EVM owner combination. Confirm the provider-supported vault roster and accepted Miden collateral with the route owner, then verify a deposit and return withdrawal on the intended deployment. An Ethereum route's readiness does not establish support on another chain, and a partner coordination group or visible market entry does not establish activation, liquidity or capacity.
Deposit into a market
The wallet's Earn code constructs TaskType.ProtocolInteraction data with the market UID, a deposit action, the market's underlying asset, and a nonzero protocol hash identifier. The Miden source account supplies the configured collateral faucet and an amount in that faucet's base units.
The workflow has distinct completion boundaries:
- Obtain a provider quote for the actual market, source collateral and intended owner.
- Authorize and submit the Miden P2IDE collateral note through the supported wallet flow.
- Track provider acceptance and the solver's destination-market interaction.
- Read the resulting position for its actual EVM owner, market and chain.
A quote, collateral-note submission, or provider acceptance alone is not proof of a completed position. When a collateral note is recallable, reclaim timing derives from the actual note/request parameters. A precomputed witness estimate is not permission to replace the note's enforced conditions with a wall-clock timer.
To withdraw through Bread, use the active Miden account’s wallet-derived EVM address as the deposit’s position owner. Depositing requires no external EVM wallet connection.
Read positions with the correct units
Bread collects EVM owner addresses from its Earn deposit rows and queries the configured positions service for those owners. An account may have used more than one owner address; the wallet does not assume a single universal “my EVM address”.
The reviewed positions reader treats deposits and withdrawable as human-decimal strings returned by that service. It does not multiply them by token decimals again. This differs from Miden collateral amounts, which are expressed in faucet base units.
Position data belongs to the configured chain, market and owner. A visible position or APR value is not proof that a particular withdrawal route is currently supported or that the rate will remain fixed.
Withdraw and return funds to Miden
Bread's supported gasless-withdrawal implementation prepares an EVM-side redemption and tracks delivery back to a specific Miden recipient and faucet. “Gasless” describes the route's transaction-submission arrangement; it does not establish the absence of provider, route, or protocol fees.
| Boundary | What it establishes |
|---|---|
| Prepared withdrawal | A particular execution identity and payload are ready. Nothing has necessarily been submitted. |
| Redemption/acceptance | The EVM-side withdrawal is being executed or accepted under that identity. |
| Delivery | The provider is fulfilling the Miden destination leg. |
| Received note | The destination note has been located; consumption and wallet sync are still separate steps. |
| Spendable balance | The destination account has consumed the correct note and reconciled its vault. |
The wallet binds prepared recovery/retry work to the source chain, EVM owner, market, recipient, destination faucet and request identity. It refuses to treat a restored backup withdrawal row as verified executable work merely because that row exists. A timeout or reload should resume/reconcile the existing operation before creating another redemption.
Fees, privacy and failure boundaries
Keep the market collateral asset separate from the network's fee asset. Creating or consuming Miden notes requires the applicable Miden fee balance; provider route terms and EVM-side charges require separate inspection.
Review the current quote and expected net amounts. Do not hard-code a fee percentage from a wallet issue, or label Earn fee-free because the UI does not itemize a charge.
The destination chain and lending provider receive the data needed for their leg, including position-owner and market information. Miden private collateral does not make the complete cross-chain interaction invisible to every provider.
If market configuration is unavailable, the position owner is wrong, or the provider cannot fulfill the route, stop before creating new collateral or redemption work. Use the provider's supported status/recovery operations and the existing Epoch note-recovery guide. Neither solver availability nor recovery after collateral consumption should be inferred from the presence of a P2IDE note alone.