UNYKORN DOCS
wallet.unykorn.ai/docs · v1.0

UNYKORN Wallet — developer reference

Integrate UNYKORN Wallet in any web dapp. Speaks EIP-1193 fully so every existing dapp works unmodified. Announces itself via EIP-6963 to any wallet-selection UI. Adds a uny_* extended namespace for what a custody rail actually does — multi-approver movements, timelocks, policy-refused intents, agent-safe treasury via x402 capability tokens.

Overview

The vault-sdk provider is not a MetaMask clone. MetaMask's model is: dapp asks for a signature, user clicks Confirm, done. That model has no vocabulary for the thing a custody platform does constantly — "this movement is valid but needs a second approver and a 24-hour timelock." A standard EIP-1193 provider can only answer that with a rejection, which is wrong and which teaches users to retry.

This provider speaks EIP-1193 fully — every existing dapp works unmodified. Where the honest answer is representable, it uses the uny_* namespace to say what the state actually is.

Quickstart

Install the SDK from npm (planned; today, source-import from the repo):

# Once published:
npm install @unykorn/vault-sdk

# Today, from source:
npm install github:FTHTrading/wallet#main --workspace-root

Import it and use the standard EIP-1193 flow:

import "@unykorn/vault-sdk";

// Legacy: window.ethereum still works
const accounts = await window.ethereum.request({
  method: "eth_requestAccounts",
});

// Preferred: EIP-6963 discovery
window.addEventListener("eip6963:announceProvider", (event) => {
  const { info, provider } = event.detail;
  if (info.rdns === "ai.unykorn.wallet") {
    // vault-native
  }
});
window.dispatchEvent(new Event("eip6963:requestProvider"));

Trust boundary

Untrusted page, trusted extension. The provider runs in the page. It holds no keys, no session secret, no policy state. Everything it does is a message across the content-script bridge to the extension service worker, which re-derives permissions from its own storage. A hostile page can call every method and learn nothing it was not granted, and move nothing policy did not allow.

EIP-1193 — standard methods

Every standard EIP-1193 method is implemented and forwarded to the selected chain provider (EVM chains via the vault-sdk's internal wallet handling; non-EVM chains via chain-specific adapters). Common ones:

MethodBehaviour
eth_requestAccountsOpens the extension popup for the operator to select which wallet the dapp gets access to.
eth_chainIdReturns the current EVM chain the wallet is scoped to.
eth_sendTransactionCreates a movement intent. If policy allows and quorum is met, it broadcasts. If quorum is unmet, throws 4911 PENDING_APPROVALS.
personal_sign, eth_signTypedData_v4Standard signing. Policy engine still evaluates the signing wallet's session state.
wallet_switchEthereumChainStandard switch. Wallet may refuse chains its policy doesn't include.

EIP-6963 — provider discovery

EIP-6963 replaces the window.ethereum monkey-patching war. Each wallet announces itself with a unique uuid, a reverse-DNS identifier, a display name, and an icon.

const providers = [];

window.addEventListener("eip6963:announceProvider", (event) => {
  providers.push({
    info: event.detail.info,      // { uuid, name, rdns, icon }
    provider: event.detail.provider,
  });
});

window.dispatchEvent(new Event("eip6963:requestProvider"));

UNYKORN Wallet's identifiers:

FieldValue
rdnsai.unykorn.wallet
nameUNYKORN Vault
uuidgenerated per extension install

uny_* extended namespace

The vocabulary EIP-1193 does not have. Standard providers can say YES or NO. UNYKORN can also say:

uny_capabilities

A dapp that wants to know whether it's talking to a vault-native provider probes this method. Vault-native providers return an object; standard providers throw 4200 UNSUPPORTED_METHOD.

try {
  const caps = await provider.request({ method: "uny_capabilities" });
  // { version: 1, extended: ["intent","approvals","timelocks","x402"] }
} catch (err) {
  if (err.code === 4200) {
    // Standard EIP-1193 wallet — fall back to eth_sendTransaction only.
  }
}

uny_intent

Explicitly create a movement intent — same shape as eth_sendTransaction but returns the intent object instead of throwing on pending approvals.

const intent = await provider.request({
  method: "uny_intent",
  params: [{
    wallet: "wal_m-helen-escrow",
    asset: "base:USDC:0xa0b869…",
    amount: "96102151",  // integer minor units (USD × 100)
    destination: "0x00000000000000000000000000000000000dEaD",
  }],
});

// {
//   status: "pending_approvals",
//   code: 4911,
//   intentId: "intent_qz3f1x8k",
//   required: 2, current: 1,
//   approvers: [{ role: "signer_plane", signed_at: "…", fingerprint: "8220…" },
//               { role: "policy_officer", signed_at: null, fingerprint: null }],
//   poll_at: "/intent/qz3f1x8k"
// }

uny_listWallets

Enumerate wallets the current session is authorised to see.

const wallets = await provider.request({ method: "uny_listWallets" });
// [{ id, name, threshold: "2-of-3", recovery_proven: true, chain, floor_reported }, …]

x402 capability token — shape

x402 is the layer neither BitGo nor MetaMask has: a signed grant that authorises an agent to CREATE INTENTS only. It cannot sign, cannot approve, cannot raise its own budget. Fully compromised, an agent can only spend its remaining budget to destinations already on its allowlist — and every unit lands on the receipt chain.

{
  header: {
    typ: "uny-cap",
    v: 1,
    iss: "<issuer public key, hex>",
  },
  claims: {
    sub: "x402-payer",           // agent subject
    enterprise: "ent_unykorn-llc",
    wallet: "wal_m-helen-escrow",
    asset: "base:USDC:0xa0b869…",
    budget: "50000000000",           // integer minor units, decimal string
    perTxCeiling: "5000000000",
    destinations: ["0x00…dEaD", "0x9c…8e"],
    notBefore: 1786100000,
    expiresAt: 1786160000,
    nonce: "cap_payables_q3",        // revocation handle
  },
  signature: "<Ed25519, hex>",
}

Signature: Ed25519 over SHA-256(domain || canonical(header) || canonical(claims)). Domain separator: unykorn.vault.capability.v1.

No JWT. Algorithm agility is the vulnerability — alg:none and RS256/HS256 confusion have each produced real custody incidents. There is one algorithm, one domain separator, no negotiation surface.

Mint a capability

import { mintCapability, generateIssuerKey } from "@unykorn/x402";

const { privateKey, publicKeyHex } = await generateIssuerKey();
const token = await mintCapability({
  sub: "payables-q3",
  enterprise: "ent_unykorn-llc",
  wallet: "wal_m-helen-escrow",
  asset: "base:USDC:0xa0b869…",
  budget: "50000000000",
  perTxCeiling: "5000000000",
  destinations: ["0x00000000000000000000000000000000000dEaD"],
  notBefore: Math.floor(Date.now() / 1000),
  expiresAt: Math.floor(Date.now() / 1000) + 21600,  // 6h
  nonce: "cap_payables_q3",
}, privateKey, publicKeyHex);

Verify a capability

import { verifyCapability } from "@unykorn/x402";

const claims = await verifyCapability(
  token,
  ["<trusted issuer public key hex>"],
  Math.floor(Date.now() / 1000),
  revokedNonces,   // Set<string> of revoked nonces
);
// Throws CapabilityError with .code for any failure (bad_signature,
// expired, revoked, untrusted_issuer, not_yet_valid, malformed).

Authorize spend + revoke

import { CapabilityLedger } from "@unykorn/x402";

const ledger = new CapabilityLedger();

const { remaining } = ledger.authorize(claims, {
  asset: "base:USDC:0xa0b869…",
  amount: "400000000",
  destination: "0x00000000000000000000000000000000000dEaD",
});
// { remaining: "49999600000" }
// Rejects (never clamps) with .code: over_per_tx_ceiling, over_budget,
// destination_not_allowed, asset_mismatch, revoked.

ledger.revoke("cap_payables_q3");   // nonce revocation, instant

Custody-native error codes

All in the 4900 application-defined range — never conflicts with EIP-1193 standard errors.

4001
USER_REJECTED — standard EIP-1193. The operator clicked Reject in the extension popup.
4100
UNAUTHORIZED — standard. Session not unlocked, or the dapp origin is not on the wallet's allowlist.
4200
UNSUPPORTED_METHOD — standard. Use this to probe for vault-native features (call uny_capabilities; catch 4200 to detect non-vault providers).
4900
DISCONNECTED — standard. Extension is not connected to a control plane.
4901
CHAIN_DISCONNECTED — standard.
4910
POLICY_DENIED — vault-specific. Movement violates a rule a signer cannot override in one click. Includes the rule name and, when safe, the value that tripped it.
4911
PENDING_APPROVALS — vault-specific. Intent created; needs additional signatures. Response includes intentId, required, current, approvers[].
4912
TIMELOCKED — vault-specific. Intent approved; cannot execute until notBefore.
4913
RECOVERY_NOT_PROVEN — vault-specific. Wallet exists but recovery ceremony not rehearsed. No funds can enter it.

Handling in dapps

try {
  await provider.request({ method: "eth_sendTransaction", ... });
} catch (err) {
  switch (err.code) {
    case 4001: // user rejected — normal, retry-safe
      break;
    case 4910: // policy denied — show the reason, do NOT auto-retry
      showPolicyReason(err.data.reason);
      break;
    case 4911: // pending approvals — render a queue, poll err.data.poll_at
      startApprovalPoll(err.data.intentId);
      break;
    case 4912: // timelocked — show countdown to err.data.notBefore
      showTimelock(err.data.notBefore);
      break;
    case 4913: // recovery not proven — direct the operator to the ceremony
      redirectToRecovery(err.data.walletId);
      break;
  }
}

Glossary

TermMeaning
allowedPolicy passed. Does not mean value moved. Custodian submission is a separate step and is currently gated.
attestation ceilingSupply hard-cap on a licensed issuer's instrument. Median of a signed verifier quorum. Recomputed per mint — never cached.
control planeThe uny-api service on 127.0.0.1:7331. Holds the policy engine, the receipts ledger, the issuance register.
enterpriseTop-level tenant scope. All wallets, capabilities and receipts are namespaced by enterprise id.
intentA proposed movement. Created by the operator or by a capability. Recorded on the receipt chain immediately, even if not yet approved.
ProvenRecoveryPlanThe only input type create_wallet accepts. Only source: a rehearsal that reconstructed the key AND proved t-1 fails.
receiptAn ML-DSA-65 signed record on the append-only ledger. Every decision produces one.
signer planeThe wallet-scoped signing service. Runs the policy engine independently of the control plane — same evaluation, different plane.
settlement venueHow an issuer's instrument settles: BookEntry, ATS, OnChainPermissioned, or OmnibusCustody.

Migrate from window.ethereum

Most integrations don't need any changes. The vault provider speaks EIP-1193 fully. But you should: