# Run a complete testnet payment

Documentation revision **2026-09-25**. This Node.js example takes one payment through catalog → quote → draft → approval if needed → burn → managed Stellar delivery. It saves the order, request IDs and pending operation before each mutation. Restarting the same directory continues the same payment.

The example supports **1–10 native test USDC, Ethereum Sepolia → Stellar testnet**, with CCTP Standard or Fast and managed destination minting. It is deliberately limited to this released onboarding path. See [compatibility](compatibility.md) before choosing your production flow.

## 1. Prepare the integration

Use Node.js 22 or later on your backend. Create a Testnet policy, enable Circle, and issue a testnet key with `catalog:read`, `quotes:read`, `quotes:write`, `payments:read` and `payments:write`. Use the [testnet guide](testnet-api.md) to fund a dedicated Sepolia sender with test ETH and native test USDC, and prepare a Stellar testnet recipient with the native test USDC trustline. Managed delivery must be available in `GET /adapters`.

Before starting, prepare these public accounts:

- An Ethereum Sepolia sender with test ETH for gas and 1–10 native test USDC. Use the exact test USDC identity returned by `GET /assets` with `Route-Network: testnet`; test USDC is available from [Circle’s faucet](https://faucet.circle.com/).
- A Stellar testnet G-account funded with [Friendbot](https://friendbot.stellar.org/) and an active trustline for the test USDC issuer returned by that same catalog. Mainnet balances and trustlines do not carry over.
- A dedicated sign-only test signer for the Sepolia sender. Managed minting needs no customer destination signer; account and trustline reserves remain your responsibility.

Download and extract the [complete starter ZIP](route-testnet-starter.zip), then run `npm install` in the extracted directory. The archive contains only these reviewed example files:

- [route-client.mjs](examples/route-client.mjs)
- [testnet-payment.mjs](examples/testnet-payment.mjs)
- [testnet-payment-flow.mjs](examples/testnet-payment-flow.mjs)
- [testnet-state.mjs](examples/testnet-state.mjs)
- [testnet-signer.mjs](examples/testnet-signer.mjs)
- [package.json](examples/package.json)
- [README.md](examples/README.md)

The two dependencies are chain signing/encoding libraries. Your application integrates no provider SDK. The signer is a reference for a dedicated backend test wallet; production signer custody and browser-wallet integration need their own review. For a browser or HSM, inject a signer into `stepTestnetPayment` that reviews the same immutable order and returns exact signed bytes **without broadcasting**.

Load these values from your local environment or secret manager. Keep both secrets out of command history, source control, browser bundles and logs:

```text
ROUTE_API_KEY=<testnet Route key>
ROUTE_TESTNET_PRIVATE_KEY=<dedicated Sepolia test-wallet key; local signer only>
ROUTE_SENDER=<that wallet's public 0x address>
ROUTE_RECIPIENT=<Stellar testnet G account with test USDC trustline>
ROUTE_BASE_URL=https://route.click/v1
ROUTE_AMOUNT=1
ROUTE_CCTP_MODE=standard
ROUTE_MAX_PROTOCOL_FEE=0.01
ROUTE_MAX_GAS_WEI=1000000000000000
```

`ROUTE_MAX_PROTOCOL_FEE` is a test-USDC cap; `ROUTE_MAX_GAS_WEI` is a **per-action** test-ETH gas ceiling (default 0.001 ETH). An approval and a burn can each consume gas. These are starter test limits, not mainnet limits or guaranteed charges. Route never receives the private key. The immutable order records public wallets and caps at initialization; changing environment variables later does not change that order.

## 2. Initialize and review

Use a new private directory for each business order, outside any served web directory:

```sh
node testnet-payment.mjs init ./private-order-001
node testnet-payment.mjs run ./private-order-001
node testnet-payment.mjs status ./private-order-001
```

The runner saves the quote and draft, prepares and registers the first wallet action, then stops at `authorization-required`. No transaction has been signed or submitted. Testnet quotes intentionally have `selected: null`; your explicit `ROUTE_CCTP_MODE` selects an available candidate, not a claim of best price or speed. If the sender already has sufficient allowance, the first action is the burn.

## 3. Authorize and track

After reviewing the saved order and fee ceilings:

```sh
node testnet-payment.mjs run ./private-order-001 --authorize
```

`--authorize` authorizes the dedicated test signer to sign **both** necessary source actions within that saved order. The reference signer checks the chain ID, sender, exact token/spender, amount, destination forwarding contract, recipient, payment binding, protocol-fee cap, nonce, review expiry and gas ceiling. It cannot sign an arbitrary transaction or broadcast by itself. Route independently checks the signed transaction before submission.

After one successful canonical approval block, the runner prepares and reviews the burn. It then tracks source finality, attestation and managed minting until `completed`. Ethereum finality can take longer than Circle Fast attestation; this testnet flow waits for source finality before minting.

Use `step` instead of `run` for a single durable step. A run is bounded to 240 steps; restart the **same** directory if it stops at `run-limit`. Tracking starts at 15 seconds, increases to 30 and 60 seconds, adds up to 20% jitter, and honors a longer `Retry-After`.

## 4. Restart and recover

```sh
node testnet-payment.mjs status ./private-order-001
node testnet-payment.mjs run ./private-order-001 --authorize
```

- A lost quote, draft or authorization response reuses the saved key and identical body. A lost signature response reuses the same authorization and bytes. A lost execution response first reads that authorization; a submitted or uncertain attempt is only tracked.
- `signed` and a `sourceHash` can exist before broadcast. Never create a second payment just because an explorer cannot find that hash.
- An expired unsigned action can be refreshed only through the server's same-nonce rules. A signed expired action pauses. This starter does not replace, cancel on-chain or raise fees on an existing transaction.
- `paused` requires inspection. After resolving a known non-submission blocker, `--resume` clears the local pause and retries the saved operation. It does not change that operation's body or bypass server checks. A stale version, changed intent, signed expiry or unresolved submission may require operator reconciliation; repeatedly resuming is not recovery.
- A crash during the signer call leaves `SIGNER_OUTCOME_UNKNOWN`. Inspect the signer and current Route authorization before changing local state. The starter refuses automatic continuation in that case.
- The store permits one runner. An interrupted process may leave `runner.lock`; verify that its recorded process is no longer running and reconcile its pending operation before removing **only the lock file**. Do not delete `payment.json` to retry.

The directory is mode 0700 and state files are mode 0600. While signature acceptance is unresolved, the private state may contain **broadcastable signed bytes**. Protect it and its backups as sensitive authorization material. Read responses and normal output omit those bytes. This file store is an example, not a replicated production job system; use a transactional store, worker lease and signer audit trail in production.

Follow the [recovery playbook](recovery.md) for support evidence and retry rules. Mainnet NEAR intent approval, review renewal and composed legs use different contracts; this testnet starter must not be switched to mainnet by changing a header.
