# Trying the flow without a key, a chain, or money

Everything below runs locally. No real wallet is involved, so a mistake costs
nothing — do this before the first real paid call, and whenever the client's
behaviour is in question.

## Offline vectors

```sh
node scripts/x402-etherlink.mjs --self-test
uv run scripts/x402_etherlink.py --self-test
```

Checks keccak-256, address derivation, the EIP-712 digest, the signature and the
signed approval transaction against fixed expectations that viem and eth-account
produced independently. All checks must report `pass: true`.

## The mock service

`harness/mock_x402.py` serves a priced API, an x402 facilitator and an Etherlink
JSON-RPC in one process. It verifies signatures with eth-account, so it accepts an
envelope only if the signature really recovers to the declared payer. The mock itself
runs under `uv` whichever client you are testing — including the `node` one.

```sh
# --default-xtz seeds every unseen address with gas for the approval below;
# without it the broadcast step correctly refuses, which is the next section.
uv run harness/mock_x402.py --port 8402 --default-xtz 5000000000000000 &

URL='http://127.0.0.1:8402/api/v6.1/128064/quote?src=0x0000000000000000000000000000000000000000&dst=0x2306c83638becff7f567d5b330602d0ddb5780af&amount=10000000000000000000'
# a public Anvil test key; worthless
export X402_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

node scripts/x402-etherlink.mjs --preflight --rpc http://127.0.0.1:8402/rpc "$URL"
node scripts/x402-etherlink.mjs --approve --broadcast-approval --rpc http://127.0.0.1:8402/rpc "$URL"
curl -s http://127.0.0.1:8402/_state        # balances, settlements, broadcast approvals
```

Every unseen address starts with 1.0 USDC, no Permit2 allowance and no XTZ — the
position a fresh agent wallet is in, and one that cannot pay until it has gas for the
approval. `--default-balance`, `--default-allowance` and `--default-xtz` change that;
`--default-xtz 5000000000000000` (0.005 XTZ) is enough for the approval.

## Injecting failures

`--fault <name>`, repeatable, makes the mock misbehave on purpose:

| fault | what the client should do |
| --- | --- |
| `no-etherlink` | refuse before signing, listing the rails that were offered |
| `wrong-asset` | refuse the unexpected token and point at `--asset` |
| `over-cap` | refuse the price against `--max-amount` |
| `short-deadline` | refuse a signature lifetime under 10s |
| `no-extension` | behave exactly as with it: the advertised sponsorship is unusable anyway |
| `settle-fail` | report `paid: false` and surface the settlement |
| `replay` | surface "nonce already spent" and not retry |
| `malformed-header` | report that a fresh 402 is needed |
| `announce-https` | refuse a scheme downgrade |

## What the mock does and does not model

It is a stand-in, not a chain. Knowing where it stops saves you from chasing a
contradiction that is an artifact:

- Only two contracts exist on its chain: Etherlink USDC and one decoy token. An
  `eth_call` to any other address **reverts**, exactly as a real node does for a
  contract that is not there. In particular the swap-side tokens (the ones in
  `/api/v6.1/{chainId}/tokens`) do not exist on Etherlink — on the real chain either —
  so `--rpc` is never a way to check their decimals.
- It implements `eth_call`, `eth_getBalance`, `eth_getTransactionCount`,
  `eth_getBlockByNumber`, `eth_chainId`, `eth_maxPriorityFeePerGas`, `eth_estimateGas`,
  `eth_sendRawTransaction` and `eth_getTransactionReceipt`. Anything else returns an
  explicit "not implemented" error rather than a plausible answer.
- Settlement moves numbers in a ledger and the settlement hash is synthetic. Approval
  transactions are the exception: they are decoded, checked for sender, target, spender
  and gas, and charged to the payer's XTZ, so the approval flow is exercised for real.
- `--approve` against the mock needs no human confirmation: the wallet is a public
  test key on a fake chain, so the Money rule about real funds does not apply.
- Its facilitator follows the **observed** behaviour of the live one, not the published
  schema: the `extensions` object must be echoed back verbatim, a schema-shaped payload
  is rejected with `extension_echo_mismatch`, the payment is charged before the request
  is validated, and an `approve()` under ~921k gas is refused by the chain. A mock built
  from the documentation instead would let all three defects ship green.

## Against the live service

An empty wallet is safe to point at the real endpoint: it holds nothing, so there is
nothing to transfer. What comes back is worth reading carefully rather than counting as
a pass. The production facilitator answers `insufficient_funds`, which shows the
envelope parsed and reached the funds check — it does **not** show the signature was
verified, because that check runs later. Signature-level confidence comes from
`--self-test`, which reproduces the deployed Permit2's own `DOMAIN_SEPARATOR()`.

## Before the first real payment

1. `--self-test` passes on the runtime you will use.
2. `--preflight` against the real endpoint reports the price you expect, and a balance
   for the wallet you intend to spend from.
3. If `permit2Allowance` is `0`, run `--approve` and check the XTZ cost it reports
   before funding the wallet with gas.

Then drop the flag.
