# The wallet, in detail

`SKILL.md` has what you need every session. This has the rest.

## The three sources, and which one to suggest

| source | set it to | what it costs |
| --- | --- | --- |
| `X402_PRIVATE_KEY` | the key itself | it has to be in the environment of every invocation, so it tends to end up in a command line and then in a transcript |
| `X402_KEY_FILE` | a path — 32 bytes of hex, or a keystore file | nothing to type, nothing to unlock; a key held in the clear is only as private as the file's mode, which is why a readable one is refused |
| the client's own wallet | nothing | encrypted at rest, but a human types a passphrase to create it and again to unlock it |

Those are the trade. No arrangement gives all three of *nobody types a passphrase*, *the
key is encrypted at rest*, and *the key never enters the agent's context* — pick the two
that matter here. For a wallet holding a research budget the usual answer is
`X402_KEY_FILE`, because the separate wallet's small balance is already the spending limit
and encryption at rest buys little on top of it.

**Someone who already has an Ethereum wallet may not need a new one.** Point
`X402_KEY_FILE` at their exported keystore and it becomes the wallet for every command
here, `unlock` and `lock` included. V3 is a container rather than one algorithm, so what
opens is a list: `aes-128-ctr` with either `pbkdf2`/`hmac-sha256` or `scrypt` — between
them what geth, MetaMask and `eth-account` write, geth's default `scrypt` included.
Anything outside the list is refused by the parameter that put it there, before the
passphrase is asked for rather than after, and both clients run that same list, so a file
either opens in both or is refused by both with the same sentence. The one bound that is
ours and not the format's: scrypt's cost parameters decide how much memory an unlock
allocates, and they come out of the file, so a keystore asking for more than 1 GiB is
refused — four times what geth's own default costs, and past anything a wallet writes.

A source that is named and then fails — a path with no file, an unreadable one, a hex key
whose mode lets anyone read it — stops the run naming the file. It never falls through to
the next source: the human funded the address they named, and paying from a different one
looks exactly like success.

## What is on disk

| what | where | mode |
| --- | --- | --- |
| the encrypted key | `$XDG_CONFIG_HOME/x402-etherlink/wallet.json` | `0600`, directory `0700` |
| the unlock window | `$XDG_RUNTIME_DIR/x402-etherlink/session.json` | `0600`, directory `0700` |

The keystore written here is a standard Ethereum V3 file — `pbkdf2`/`hmac-sha256` at
262144 rounds, `aes-128-ctr`, and a keccak256 MAC that is verified *before* the cipher
runs, so a wrong passphrase fails instead of producing a plausible key at an address that
is not yours. MetaMask, geth and `eth-account` all open it as-is. One shape is written;
more than one is read, since a keystore named with `X402_KEY_FILE` came from whichever
wallet exported it.

The window holds the decrypted key while the wallet is unlocked. It is written only to a
directory whose mount is `tmpfs` — RAM, gone at reboot — and the client checks the mount
rather than trusting the path, because `/tmp` is a real disk on plenty of machines. Where
no tmpfs exists the client does not cache at all and says so; it never falls back to disk.

Both clients share one window, so an unlock done with `uv` is honoured by `node` and the
other way round. A window that belongs to a different keystore is refused rather than used.
A window whose file has become readable by anyone else is refused and **not** repaired —
the key may already have been read, and quietly restoring the mode would hide that.

## The window's life

Fifteen minutes, refreshed by **signing**, not by looking. A status read leaves it exactly
as it found it, which is why an agent that polls while idle cannot hold a key open for
hours. Continuous work never re-prompts; a pause re-locks.

That is also why the funding ask names the order it does. Getting hold of USDC takes longer
than fifteen minutes, so a wallet unlocked before the transfer is locked again by the time
the money lands. Sending the human back a third time is the failure mode; asking for the
unlock *after* the transfer, in the same sitting, is what prevents it.

## Backup, and the absence of an export

There is no command that reveals the private key, and there will not be one. The keystore
file plus the passphrase **is** the backup: copy the file somewhere safe, keep the
passphrase apart from it, and it opens in any Ethereum wallet if the funds are ever wanted
back.

Losing the passphrase loses the money at that address, permanently. That is not a flaw to
work around — it is the reason this wallet should only ever hold what the agent is allowed
to spend. A separate wallet with a small balance is the spending limit; a cap inside the
client would only be theatre, since a stolen key is used directly and never through us.

Do not read the keystore or the window yourself. They exist for the client, and anything an
agent opens ends up in a transcript.

## Gas, and why the ask sometimes has two assets

Paying is pure USDC. But a wallet that has never paid on Etherlink needs one setup
transaction first — a Permit2 approval — and that transaction is the wallet's own to send,
so it needs a little XTZ for gas, about 0.0023 once and never again.

Some endpoints sponsor that approval. Whether *this* one does can only be known by asking
it, which is why `permit2.sponsoredApproval` reads `unknown` until a URL has been supplied
and `probedVia` names the endpoint that answered. When it is unknown the funding ask
includes the XTZ and says it may turn out to be unnecessary — an honest overestimate beats
a second trip to the terminal.

The gas figure and the shortfall are derived, never chosen: `approvalGasXtz` is the chain's
own estimate at the current fee market, and `xtzShortfall` is exactly what the wallet is
missing of it. If either looked like a round number, it would be a guess about money someone
is about to send.

## States you will see less often

- **`unsafe-session`** — the window exists but is readable by more than its owner. Ask for
  `wallet lock` then `wallet unlock`; do not use it, and do not try to fix the mode.
- **`chain-unreadable`** — the RPC could not be read. Balances come back `null`, never zero,
  and `ok` is false. Report that you could not look, which is a different fact from an empty
  wallet and leads to a different action.
- **`insufficient`** — there is USDC but less than this endpoint charges. Only reachable
  when a URL was supplied, since without a price there is nothing to be insufficient for.

## Running unattended

`X402_WALLET_PASSWORD`, when it is already set, lets `create` and `unlock` run with no
terminal, and the output says so in `warnings`. It exists so a human can arrange an
unattended run in advance. An agent never sets it: a passphrase the agent chose is a wallet
the human cannot open, and a secret written into a transcript.

**Those two commands are the whole of its reach.** The unlock is a step of its own, so an
unattended run is `wallet unlock` and then the paid call — two commands, neither of them
needing a terminal, in both clients. A paid call signs from the window or not at all: it
never opens the keystore itself, whatever is in the environment when it runs.

That is what keeps the rest of this page true. `wallet status` reads the window, so
`locked` means the next paid call fails — always, and not "unless a variable happens to be
set", which is a status screen lying about the thing it exists to answer. `wallet lock`
ends signing on the spot instead of being undone by something exported an hour ago. And
the fifteen minutes still mean fifteen minutes: a passphrase left in the environment would
otherwise hold the wallet open for the life of the shell, with no window to expire and
nothing to refresh. A run that pays continuously never re-locks; one that pauses longer
unlocks again with the same command, still with no terminal.

`X402_PRIVATE_KEY` still works and still takes precedence when set. It is the CI path — no
keystore, no window, no passphrase, and nothing to unlock first. `X402_KEY_FILE` pointed at
a hex key is the same thing with the key off the command line.
