AArtery
On this page

Security Architecture

Artery's security posture rests on three claims:

  1. Artery cannot withdraw your funds. All credentials we hold or transit grant trade-only authority.
  2. Stored credentials are encrypted under KMS. Disk compromise alone is insufficient to use them.
  3. Every signing operation is audited. Logs hash request payloads so we can prove what happened without leaking what was signed.

Crypto modules

ModuleAlgorithmUsed for
@artery/signing.hmacHMAC-SHA256Webhook verification, API key fingerprinting
@artery/signing.rsaRSA-PSS (SHA-256, salt 32)Kalshi request signing
@artery/signing.eip712EIP-712 typed data via viemHL approveAgent, Polymarket Safe Module
@artery/signing.ecdsasecp256k1 with v normalization (0/1 → 27/28)Hyperliquid order signing

The v normalization is required because viem returns v ∈ {0, 1} while Hyperliquid's mainnet expects v ∈ {27, 28}. The signing helper handles it once; callers never see raw v.

API key model

POST /keys → { plaintext: "art_live_<uuid>.<32B-base64url>" }

Server-side:

plaintext  = art_live_<uuid>.<secret>
stored     = scrypt(secret, salt = uuid, N = 2^14, r = 8, p = 1)
hash_check = constant-time compare(scrypt(input_secret), stored)

Properties:

  • The plaintext is shown once. Lose it → create a new key.
  • scrypt-64 is memory-hard — brute-forcing one stored hash on a leaked DB costs ~16 MB of RAM per attempt.
  • The <uuid> part is the lookup key, so scrypt is per-record.

KMS envelope encryption (Kalshi PEM)

The only credential we persist is a Kalshi RSA PEM. It's stored under envelope encryption:

   ┌─────────────────────────────────────────────────────┐
   │  Cloud KMS (or LocalKmsClient in dev)               │
   │  ┌────────────────────┐                             │
   │  │ Master KEK          │  never leaves KMS HSM      │
   │  └────────────────────┘                             │
   └────────▲────────────────────┬───────────────────────┘
            │                    │
       wrap(DEK)            unwrap(wrappedDEK)
            │                    │
   ┌────────┴───────────┐ ┌──────┴──────────────┐
   │ encrypt PEM        │ │ decrypt PEM         │
   │ AES-256-GCM        │ │ AES-256-GCM         │
   │ DEK = randomBytes  │ │ DEK in memory only  │
   │ 32                 │ │ for one signing call│
   └────────────────────┘ └─────────────────────┘

Stored bytes per credential:

FieldTypePurpose
ciphertextbytesAES-GCM(PEM)
iv12 bytesGCM nonce
tag16 bytesGCM auth tag
wrappedDekbytesKMS-wrapped DEK
kmsKeyIdstringwhich KMS key was used (for rotation)

Decryption flow on a Kalshi sign call:

tsconst dek = await kms.decrypt(record.wrappedDek); // KMS API call
const pem = aesGcm(dek).decrypt(record.ciphertext, record.iv, record.tag);
const sig = await rsaPssSign(pem, payload);
zeroize(dek);
zeroize(pem);
return sig;
Warning

KMS is the only authority that can unwrap DEKs. A full database leak yields ciphertext + wrapped DEK only. Without KMS access, the PEM is not recoverable.

Local development KMS

For dev, LocalKmsClient reads a 32-byte master key from KMS_LOCAL_MASTER_KEY_HEX env var and performs the wrap/unwrap in-process:

bashexport KMS_LOCAL_MASTER_KEY_HEX=$(openssl rand -hex 32)
pnpm --filter @artery/api dev

turbo.json lists this var in globalPassThroughEnv so subprocess tasks inherit it.

Note

The local master key must persist across restarts (otherwise existing ciphertexts become unreadable). Store in your secret manager (pass, 1password, etc.) and source on shell init.

Provider credential paths

PathHeldEncrypted at restReaches Artery
User master walletNevern/aNever
HL agent private keyPer-request headern/a (transient)Yes — used and dropped
Polymarket Safe ownerPer-request headern/a (transient)Yes — used and dropped
Kalshi RSA PEMPersistedKMS envelope (AES-256-GCM + KMS-wrap)Yes

Audit log

Every signing operation emits a structured record:

json{
  "ts": "2026-05-08T12:00:00.000Z",
  "level": "info",
  "event": "credential.sign",
  "userId": "u-1",
  "provider": "kalshi",
  "endpoint": "POST /trade-api/v2/portfolio/orders",
  "payloadHash": "sha256:c2a4…",
  "requestId": "req_01HMPK…"
}

The payloadHash lets auditors prove which payload was signed without storing the payload itself.

Threat model summary

Attacker capabilityRecoverable secrets
Read DBNone (ciphertext + wrappedDEK only)
Read DB + read KMS auditNone (audit is read-only on KMS API calls, no key material)
Compromise one processDEK in memory for that process during one signing op (millisecond window)
Compromise KMSAll Kalshi PEMs decryptable
Compromise KMS + DBAll Kalshi PEMs decryptable; HL agent keys still safe (per-request only)

Withdrawals require master wallet signatures. No level of Artery compromise enables withdrawals.

See also

Edit this page on GitHubLast updated
Security Architecture · Artery API Docs