Matrix logo

Wallet

The wallet subsystem provides signing backends (self-hosted and embedded) with a policy gate that enforces spend caps, contract allow-lists, and chain allow-lists before every signature.

Source files: internal/wallet/wallet.go, internal/wallet/embedded.go

The wallet subsystem provides signing backends with a policy gate. It supports two modes: self_hosted (operator holds the key locally) and embedded (delegates to the Paxeer embedded wallet over the agent-native DID lane). Every signature passes through a Gate that enforces spend caps, contract allow-lists, and chain allow-lists.


Design decisions

Two wallet modes

Self-hosted (self_hosted): Signs locally with an operator-held key. Three signer backends:

SignerConfig keySource
rawwallet.self_hosted.private_keyRaw hex private key (last resort; file must be 0600)
envwallet.self_hosted.private_keyReads from env var reference
keystorewallet.self_hosted.keystore_path + keystore_passwordDecrypts a geth v3 web3 secret-storage JSON

Backwards-compatible env shims: TACHYON_DEV_PRIVATE_KEY or TACHYON_ALLOW_DEV_SIGNER=true selects self-hosted mode automatically.

Embedded (embedded): Delegates signing and broadcasting to the Paxeer embedded wallet over the agent-native DID lane. The daemon's ed25519 seed proves a did:matrix:<label>:<keyfp> identity; the EVM key stays server-side and the wallet enforces custody policy (frozen / read_only / spend caps / allow-lists).

Two operating modes within embedded:

  • Single-tenant (keyfile set): loads the ed25519 seed, derives the agent DID, and authenticates with it. Used when one tachyond instance serves one agent.
  • Multi-tenant token-only (keyfile empty): holds no seed. Every signing request must carry a forwarded TxIntent.AuthToken (the per-agent bearer minted upstream). This is the shared-engine deployment: many agents, zero seeds on the box.

Backwards-compatible env shims: PAXEER_WALLET_TOKEN or MATRIX_EXECUTOR_KEY selects embedded mode automatically.

Policy gate (Gate)

The Gate wraps a Signer and a map of named Policy profiles:

type Gate struct {
    Signer   Signer
    Profiles map[string]Policy
}

type Policy struct {
    Name             string
    SpendCapWei      *big.Int
    AllowedContracts []common.Address
    AllowedChains    []string
    ChainID          string
}

Authorize(token, requestCap, chainID) resolves a capability token to an effective policy:

  • When profiles are configured, an unknown token is denied
  • The request's spend cap can only tighten the profile's cap, never raise it
  • Chain and contract allow-lists are enforced

Sign(ctx, client, intent, policy) validates the policy against the intent, then delegates to the signer.

Signer interface

type Signer interface {
    Sign(ctx context.Context, client *evm.Client, intent TxIntent) (SignResult, error)
    Address(ctx context.Context) (common.Address, error)
}

SignResult

Exactly one of RawTx / TxHash is populated:

  • RawTx: a locally signed transaction the caller must broadcast (self-hosted mode)
  • TxHash: the signer already broadcast (remote send); caller waits for receipt (embedded mode)

Embedded wallet handshake

The embedded signer authenticates via ed25519 challenge/verify:

POST /v1/agent/auth/challenge {did} -> {message, nonce}
ed25519-sign(message)
POST /v1/agent/auth/verify {did, public_key, nonce, signature} -> {token}
POST /v1/agent/send {tx} (Bearer token) -> {tx_hash, address}

The token is cached and refreshed automatically on 401 responses.

TxIntent

type TxIntent struct {
    From      string
    To        string     // "" => contract creation
    Data      []byte
    Value     *big.Int
    Gas       uint64     // 0 => estimate
    AuthToken string     // forwarded embedded-wallet bearer for multi-tenant
}

NewGate construction

func NewGate(cfg config.Config) (*Gate, error)
  • self_hosted mode: creates a LocalSigner from the wallet config
  • embedded mode: creates an EmbeddedSigner from the wallet config
  • Default (no mode): returns a nil-signer gate (read-only verbs still work; signing verbs return WALLET_NOT_CONFIGURED)

Policy profiles are built from config.Policies (policy.* sections in tachyon.config.kvx).


Modifying the wallet

What to changeWhere
Add signer backendNew type implementing Signer interface
Add policy checkinternal/wallet/wallet.go - validatePolicy
Change embedded handshakeinternal/wallet/embedded.go - authenticate
Add new wallet modeinternal/config/config.go - WalletMode* constants + NewGate switch