Tutorial: your first confirmed transfer
This page walks one transfer from a test key to a confirmed status. It assumes no prior Mure code. Each step names what you should see before you continue.
The whole run happens on Sepolia (eip155:11155111), so no real funds move.
1. Get a test key
Test keys are issued by the Mure team. Ask for one at contact Mure and name the testnet you target. There is no self-serve signup while the account app is rebuilt.
A test key starts with sk_test_ and works only on testnet chains. A sk_live_ key on the
same request answers 403 with the
chain-not-allowed-for-key-environment
problem.
Keep the key out of the repository. The examples below read it from the environment.
export MURE_API_KEY="sk_test_..."2. Fund the account
You need two balances on Sepolia:
- Sepolia ETH for gas. The transaction is paid by the account that sends it.
- Test USDC for the amount you move. This tutorial moves 1 USDC.
Fund the account from a Sepolia faucet, or from another Sepolia account you control. Watch the transaction on a Sepolia explorer before you continue: a transfer with no gas fails at submission, not at the API.
3. Install and configure the SDK
npm install @mure/sdk viem effectimport { createClient, viem } from "@mure/sdk";
import { sepolia } from "@mure/sdk/accounts/evm";
import { http } from "viem";
import { sepolia as sepoliaChain } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const adapter = viem({
chain: sepoliaChain,
transport: http(),
account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
});
const client = createClient({
adapter,
api: { apiKey: process.env.MURE_API_KEY },
});A keyless client is allowed, but every /v2 route except GET /health answers 401
without one. See Authentication for the header and key format.
4. Create the intent
createTransfer builds the intent and returns it without sending anything. Read the
response before you commit: status is draft, and tx holds the payload that will be
broadcast.
const intent = await client.createTransfer({
to: sepolia("0x8f664a25158021EFb4470f9036431664ce9f7895"),
asset: "USDC",
amount: 1_000_000n, // 1 USDC, in base units
});
console.log(intent.id); // int_transfer_...
console.log(intent.status); // draftAn amount is always an integer string in the token's base units, so 1 USDC is
1_000_000. A swap or a bridge uses the same call with an { in, out } asset; see
Getting Started.
5. Submit the transaction
const [outcome] = await client.send(intent.tx);
console.log(outcome.status); // success, reverted, failed, or unknownSubmission and confirmation are separate steps. A submitted transaction does not mean a settled transfer: the API has not seen it yet. The status changes when the indexer picks the order up.
6. Poll until the status is terminal
Read the intent by id until it reaches a terminal state. Two seconds between reads keeps a single poller under the 50 requests per minute limit that Authentication documents.
const terminal = new Set(["confirmed", "finalized", "failed", "expired"]);
let transfer = await client.getTransfer({ id: intent.id });
while (!terminal.has(transfer.status)) {
await new Promise((resolve) => setTimeout(resolve, 2000));
transfer = await client.getTransfer({ id: intent.id });
}
if (transfer.status === "failed" || transfer.status === "expired") {
throw new Error(`Transfer ended as ${transfer.status}`);
}
console.log(transfer.status); // confirmed, later finalizedThe same read with curl:
curl "https://api.mure.app/v2/transfers/$INTENT_ID" \
-H "api-key: $MURE_API_KEY"The six statuses
| Status | Meaning |
|---|---|
draft | The intent exists and nothing has been submitted. |
pending | The transaction is submitted and the indexer has not confirmed execution. |
confirmed | The indexer confirmed execution across every chain the intent touches. |
finalized | Terminal. The finalization sweep has closed a confirmed intent. Treat it as success. |
failed | Execution reverted, or the deadline elapsed without settlement. |
expired | The deadline passed before submission, so the order can no longer execute. |
confirmed and finalized are success. failed and expired are terminal failures: the
intent does not recover on its own, and re-submitting the same tx does not help.
When a step fails
401or403at create: the key is missing, wrong, or bound to the other environment. Check the prefix against the chain.403chain-not-allowed-for-key-environment: ask_test_key on a mainnet chain, or the reverse.422validation-failed: a field fails the schema. The response names the field.429rate-limited: wait the number of seconds inRetry-After, then resume. Do not retry a create call. See Retries and polling.- The create response never arrives: do not send the request again. A create call has no idempotency key, so a second POST can create a second intent. List your transfers and match the intent you expected.
Next
- Retries and polling: the retry rule for every call, and why a create is the one call you never repeat.
- Client: the full SDK surface, including
createTransferandsend. - Intent Lifecycle: the state machine behind the six statuses.