Skip to content
LogoLogo

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 effect
import { 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); // draft

An 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 unknown

Submission 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 finalized

The same read with curl:

curl "https://api.mure.app/v2/transfers/$INTENT_ID" \
  -H "api-key: $MURE_API_KEY"

The six statuses

StatusMeaning
draftThe intent exists and nothing has been submitted.
pendingThe transaction is submitted and the indexer has not confirmed execution.
confirmedThe indexer confirmed execution across every chain the intent touches.
finalizedTerminal. The finalization sweep has closed a confirmed intent. Treat it as success.
failedExecution reverted, or the deadline elapsed without settlement.
expiredThe 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

  • 401 or 403 at create: the key is missing, wrong, or bound to the other environment. Check the prefix against the chain.
  • 403 chain-not-allowed-for-key-environment: a sk_test_ key on a mainnet chain, or the reverse.
  • 422 validation-failed: a field fails the schema. The response names the field.
  • 429 rate-limited: wait the number of seconds in Retry-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 createTransfer and send.
  • Intent Lifecycle: the state machine behind the six statuses.