justaname-id/cryptoskills · Archived

jaw

Build passkey-authenticated smart accounts on EVM chains with JAW SDK.

First seen Aug 4, 2026

Installation

$ npx skills add justaname-id/cryptoskills --skill jaw

Summary

  • Build passkey-authenticated smart accounts on EVM chains with JAW SDK.
  • ERC-4337 smart accounts with WebAuthn passkey signers, gasless transactions, batch operations, and ERC-7715 permissions.
  • Use when building dApps with passwordless wallet auth, subscription payments, stablecoin transfers, or gasless UX.
  • Supports wagmi (React), vanilla JS, and headless server-side.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from justaname-id/cryptoskills · top by installs.

npx skills add justaname-id/cryptoskills

Browse all from justaname-id/cryptoskills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Declared
Gemini CLI Not declared
Cline Declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0
LicenseApache-2.0
CompatibilityClaude Code, Cursor, Windsurf, Cline
Declared agents claude-code cursor windsurf cline
More metadata
author
JustaName-id
version
1.0
chain
multichain
category
Infrastructure

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 22,481 B
  • docs SUMMARY.md 379 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

JAW SDK

JAW SDK provides passkey-authenticated smart accounts (ERC-4337) on any EVM chain. It is an EIP-1193 compatible provider — a drop-in replacement for MetaMask — with passkey signers, gasless transactions via paymasters, atomic batch operations, and delegated permissions (ERC-7715).

Three packages:

  • @jaw.id/wagmi — React/Next.js integration with wagmi hooks
  • @jaw.id/core — Vanilla JS, Node.js, and headless server-side usage
  • @jaw.id/ui — Pre-built UI components for AppSpecific auth mode

API key required from <https://dashboard.jaw.id>;.

What You Probably Got Wrong

AI agents have stale training data. This section corrects the most common mistakes.

  • Importing useConnect/useDisconnect from wagmi → Import them from @jaw.id/wagmi, not from wagmi. The JAW versions support capabilities (SIWE, subnames) that wagmi's hooks do not.
  • Calling disconnect() with no args → You must pass an empty object: disconnect({}). Omitting it causes a runtime error.
  • Using Date.now() for permission expiry → Expiry must be Unix seconds, not milliseconds. Use Math.floor(Date.now() / 1000) + durationInSeconds.
  • Setting both selector AND functionSignature in call permissions → Use one or the other, never both. The SDK throws if you provide both.
  • Encoding calldata as a string like 'transfer(0xAlice, 1000000)' → Always use encodeFunctionData from viem. Raw strings are not valid calldata.
  • Assuming sendCalls waits for confirmationsendCalls returns immediately with a user operation ID. You must poll getCallStatus for completion.
  • Hardcoding paymaster URLs → Import JAWPAYMASTERURL from the SDK. Never hardcode the URL.
  • Using personalsign without hex-encodingpersonalsign requires hex input via toHex(). Use wallet_sign instead to avoid encoding issues.
  • Mixing Account API with wagmi in the same flow → Pick one approach per flow. The Account API (@jaw.id/core) and wagmi hooks (@jaw.id/wagmi) should not be mixed in a single user flow.
  • Omitting transports for configured chains → Missing transports in createConfig causes silent connection failures. Every chain in chains must have a matching transport.
  • Using wagmi's useSignMessage/useSignTypedData → Use useSign from @jaw.id/wagmi instead. It is the unified hook for all signing.

Quick Start

Installation

React / Next.js:

npm install @jaw.id/wagmi wagmi viem @tanstack/react-query

Vanilla JS / Server-side:

npm install @jaw.id/core viem

AppSpecific mode (optional UI components):

npm install @jaw.id/ui

viem is a required peer dependency for all JAW packages.

Wagmi Configuration

import { createConfig, http } from "wagmi";
import { mainnet, base } from "wagmi/chains";
import { jaw } from "@jaw.id/wagmi";

export const config = createConfig({
  chains: [mainnet, base],
  connectors: [
    jaw({
      apiKey: process.env.NEXT_PUBLIC_JAW_API_KEY!,
      appName: "My App",
      appLogoUrl: "https://example.com/logo.png", // HTTPS, min 200x200
    }),
  ],
  transports: {
    [mainnet.id]: http(),
    [base.id]: http(),
  },
});

App Provider Setup

import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "./config";

const queryClient = new QueryClient();

export default function App({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WagmiProvider>
  );
}

Auth Modes

CrossPlatform (Default)

Operations happen in a keys.jaw.id popup. Wallets are portable across dApps.

const connector = jaw({ apiKey: "YOUR_API_KEY" });

AppSpecific

Operations happen in your app. Requires a uiHandler for rendering passkey prompts. White-label experience.

import { jaw, Mode } from "@jaw.id/wagmi";
import { ReactUIHandler } from "@jaw.id/ui";

const connector = jaw({
  apiKey: "YOUR_API_KEY",
  preference: {
    mode: Mode.AppSpecific,
    uiHandler: new ReactUIHandler(),
  },
});

Connect and Disconnect

Always import from @jaw.id/wagmi, not from wagmi:

import { useConnect, useDisconnect } from "@jaw.id/wagmi";
import { useAccount } from "wagmi";

function ConnectButton() {
  const { connect, connectors } = useConnect();
  const { disconnect } = useDisconnect();
  const { isConnected, address } = useAccount();

  if (isConnected) {
    return (
      <div>
        <p>Connected: {address}</p>
        <button onClick={() => disconnect({})}>Disconnect</button>
      </div>
    );
  }

  return (
    <button onClick={() => connect({ connector: connectors[0] })}>
      Connect Wallet
    </button>
  );
}

For capabilities (SIWE, subnames), use walletconnect method, not ethrequestAccounts.

Transactions

Single Transaction (Wagmi)

import { useSendTransaction } from "wagmi";
import { parseEther } from "viem";

function SendETH() {
  const { sendTransaction, isPending } = useSendTransaction();

  return (
    <button
      onClick={() =>
        sendTransaction({
          to: "0xRecipientAddress",
          value: parseEther("0.01"),
        })
      }
      disabled={isPending}
    >
      {isPending ? "Sending..." : "Send 0.01 ETH"}
    </button>
  );
}

Batch Transactions (Wagmi)

All calls in a batch are atomic — they all succeed or all revert.

import { useSendCalls } from "wagmi";
import { parseEther, encodeFunctionData, erc20Abi } from "viem";

function BatchTransfer() {
  const { sendCalls, isPending } = useSendCalls();

  const handleBatch = () => {
    sendCalls({
      calls: [
        { to: "0xAliceAddress", value: parseEther("0.01") },
        { to: "0xBobAddress", value: parseEther("0.02") },
        {
          to: "0xUSDC_CONTRACT",
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "transfer",
            args: ["0xCharlieAddress", 1000000n], // 1 USDC (6 decimals)
          }),
        },
      ],
    });
  };

  return (
    <button onClick={handleBatch} disabled={isPending}>
      {isPending ? "Processing..." : "Batch Transfer"}
    </button>
  );
}

Core Provider Transactions

// Single transaction
const txHash = await jaw.provider.request({
  method: "eth_sendTransaction",
  params: [
    {
      to: "0xRecipientAddress",
      value: "0x2386F26FC10000", // 0.01 ETH in hex wei
    },
  ],
});

// Batch transaction
const result = await jaw.provider.request({
  method: "wallet_sendCalls",
  params: [
    {
      calls: [
        { to: "0xAliceAddress", value: "0x2386F26FC10000" },
        { to: "0xBobAddress", value: "0x470DE4DF820000" },
      ],
    },
  ],
});

Headless Account API

For server-side or heaadless flows using @jaw.id/core:

import { Account } from "@jaw.id/core";
import { parseEther, encodeFunctionData, erc20Abi } from "viem";

// Get existing account (returning user)
const account = await Account.get({
  chainId: 8453, // Base
  apiKey: "YOUR_API_KEY",
});

// Single transaction — waits for receipt, returns tx hash
const hash = await account.sendTransaction([
  { to: "0xRecipientAddress", value: parseEther("0.1") },
]);

// Batch transaction — returns immediately with operation ID
const { id, chainId } = await account.sendCalls([
  { to: "0xAliceAddress", value: parseEther("0.01") },
  {
    to: "0xUSDC_CONTRACT",
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: "transfer",
      args: ["0xBobAddress", 5000000n],
    }),
  },
]);

// Poll for batch completion
const status = await account.getCallStatus(id);
// 100 = Pending, 200 = Completed, 400 = Offchain failure, 500 = Onchain revert

Signing

Use useSign from @jaw.id/wagmi — the unified hook for personal sign and typed data:

import { useSign } from "@jaw.id/wagmi";

function SignMessage() {
  const { sign, isPending } = useSign();

  // Personal sign — type 0x45 (EIP-191)
  const handleSign = async () => {
    const signature = await sign({ type: "0x45", message: "Hello JAW!" });
    console.log("Signature:", signature);
  };

  // Typed data sign — type 0x01 (EIP-712)
  const handleTypedData = async () => {
    const signature = await sign({
      type: "0x01",
      typedData: {
        domain: { name: "MyApp", version: "1", chainId: 1 },
        types: {
          Order: [
            { name: "amount", type: "uint256" },
            { name: "token", type: "address" },
          ],
        },
        primaryType: "Order",
        message: { amount: 1000000n, token: "0xA0b8..." },
      },
    });
    console.log("Typed signature:", signature);
  };

  return (
    <div>
      <button onClick={handleSign} disabled={isPending}>
        Sign Message
      </button>
      <button onClick={handleTypedData} disabled={isPending}>
        Sign Typed Data
      </button>
    </div>
  );
}

The chainId parameter in useSign controls which chain's smart account signs. This is different from domain.chainId in EIP-712 typed data.

Provider-Level Signing

import { toHex } from "viem";

// personal_sign — MUST hex-encode the message
const sig = await jaw.provider.request({
  method: "personal_sign",
  params: [toHex("Hello JAW!"), address],
});

// wallet_sign — personal sign (type 0x45)
const sig2 = await jaw.provider.request({
  method: "wallet_sign",
  params: [{ type: "0x45", message: "Hello JAW!" }],
});

// wallet_sign — typed data (type 0x01)
const sig4 = await jaw.provider.request({
  method: "wallet_sign",
  params: [{ type: "0x01", typedData }],
});

// eth_signTypedData_v4 — must JSON.stringify the typed data
const sig3 = await jaw.provider.request({
  method: "eth_signTypedData_v4",
  params: [address, JSON.stringify(typedData)],
});

Permissions (ERC-7715)

Grant delegated permissions so a server can act on behalf of the user without repeated passkey prompts.

import { useGrantPermissions, useRevokePermissions } from "@jaw.id/wagmi";

function PermissionsManager() {
  const { grantPermissions } = useGrantPermissions();
  const { revokePermissions } = useRevokePermissions();

  const handleGrant = async () => {
    const result = await grantPermissions({
      expiry: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days (SECONDS)
      signer: {
        type: "account",
        data: { id: "0xSpenderAddress" },
      },
      permissions: [
        {
          type: "call-permission",
          data: {
            to: "0xUSDC_CONTRACT",
            functionSignature: "transfer(address,uint256)", // OR selector, not both
          },
        },
        {
          type: "spend-permission",
          data: {
            token: "0xUSDC_CONTRACT",
            allowance: "10000000", // 10 USDC (string, in smallest unit)
            period: "month",
          },
        },
      ],
    });

    // MUST store this — needed for server-side usage and revocation
    const permissionId = result.permissionId;
    console.log("Permission granted:", permissionId);
  };

  const handleRevoke = async (permissionId: string) => {
    // Revocation is permanent and costs gas
    await revokePermissions({ permissionId });
  };

  return (
    <div>
      <button onClick={handleGrant}>Grant Permission</button>
      <button onClick={() => handleRevoke("stored-permission-id")}>
        Revoke Permission
      </button>
    </div>
  );
}

For native ETH spend permissions, use the ERC-7528 sentinel address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.

Server-Side Permission Usage

import { Account } from "@jaw.id/core";

// Spender key MUST be in a secrets manager or HSM, never in source code
const account = Account.fromLocalAccount({
  privateKey: process.env.SPENDER_PRIVATE_KEY!,
  chainId: 8453,
  apiKey: process.env.JAW_API_KEY!,
});

const { id } = await account.sendCalls(
  [
    {
      to: "0xUSDC_CONTRACT",
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "transfer",
        args: ["0xMerchantAddress", 5000000n],
      }),
    },
  ],
  { permissionId: "stored-permission-id" },
);

Subscription Payments Pattern

  1. User grants permission via useGrantPermissions with expiry matching subscription duration
  2. Store permissionId in your backend database
  3. Server charges periodically using Account.fromLocalAccount() with spender private key
  4. User cancels via useRevokePermissions
  5. Monitor charge status with getCallStatus, implement retry and alerting logic

Gas Sponsoring (Paymasters)

Stablecoin gas payments (e.g. pay gas in USDC) work out of the box — no paymaster configuration needed from developers. Users can pay gas in supported ERC-20 tokens natively.

For fully sponsored (gasless) transactions where the dApp covers gas, configure paymasters per chain in the connector. Requires a paymaster that supports EntryPoint v0.8 and EIP-7677. Compatible providers: Pimlico, Etherspot.

const connector = jaw({
  apiKey: "YOUR_API_KEY",
  paymasters: {
    1: {
      // Ethereum mainnet
      url: "https://api.pimlico.io/v2/1/rpc?apikey=YOUR_PIMLICO_KEY",
      context: { sponsorshipPolicyId: "your-policy-id" },
    },
    8453: {
      // Base
      url: "https://api.pimlico.io/v2/8453/rpc?apikey=YOUR_PIMLICO_KEY",
      context: { sponsorshipPolicyId: "your-policy-id" },
    },
  },
});

Per-transaction override:

const result = await jaw.provider.request({
  method: "wallet_sendCalls",
  params: [
    {
      calls: [{ to: "0xRecipient", value: "0x2386F26FC10000" }],
      paymasterService: {
        url: "https://custom-paymaster.example.com",
        context: { policyId: "override-policy" },
      },
    },
  ],
});

Account Lifecycle (Headless)

import { Account } from "@jaw.id/core";

// New user — triggers WebAuthn registration
const newAccount = await Account.create({
  chainId: 8453,
  apiKey: "YOUR_API_KEY",
});
// MUST store credentialId for future authentication
const credentialId = newAccount.credentialId;

// Returning user — pass stored credentialId
const returning = await Account.get({
  chainId: 8453,
  apiKey: "YOUR_API_KEY",
  credentialId: credentialId,
});

// Server-side — no passkey, uses private key
const serverAccount = Account.fromLocalAccount({
  privateKey: process.env.PRIVATE_KEY!,
  chainId: 8453,
  apiKey: process.env.JAW_API_KEY!,
});

// Gas estimation
const gas = await returning.estimateGas([
  { to: "0xRecipient", value: parseEther("0.1") },
]);

// Account metadata (null for local accounts)
const metadata = await returning.getMetadata();

SIWE (Sign-In with Ethereum)

Backend Endpoints Required

Your backend needs three endpoints:

  • GET /api/siwe/nonce — generate unique nonce with generateSiweNonce() from viem
  • POST /api/siwe/verify — verify signature with verifySiweMessage() and parseSiweMessage()
  • POST /api/siwe/logout — invalidate session

Store nonces server-side and invalidate after use.

Frontend: Connect with SIWE

Use walletconnect with the signInWithEthereum capability — not ethrequestAccounts:

const result = await jaw.provider.request({
  method: "wallet_connect",
  params: [
    {
      capabilities: {
        signInWithEthereum: {
          nonce: nonceFromServer,
          chainId: "0x1", // MUST be hex string, not number
        },
      },
    },
  ],
});

ENS Identity

ENS operations use @justaname.id/sdk — a separate package, not part of @jaw.id/*:

npm install @justaname.id/sdk
import { JustaName } from "@justaname.id/sdk";

const justaname = new JustaName({ apiKey: "YOUR_JUSTANAME_KEY" });

// Read records (no API key needed)
const records = await justaname.getRecords({ ens: "user.eth" });

// Reverse resolve
const name = await justaname.reverseResolve({ address: "0x..." });

// Update subname (requires SIWE auth + ensDomains + apiKey)
await justaname.updateSubname({
  ens: "sub.domain.eth",
  text: { "com.twitter": "@handle" },
  coins: { ETH: "0xNewAddress" },
});

Error Handling

Code Meaning Action
4001 User rejected request Normal flow — do NOT show error toast
4100 Unauthorized Check auth state, reconnect if needed
4200 Unsupported method Verify method name and provider version
4900 Disconnected Provider lost connection, prompt reconnect
4901 Chain disconnected Switch to a connected chain
4902 Unrecognized chain Add chain to config
-32700 Parse error Check request payload format
-32603 Internal error Retry or report bug
try {
  const result = await jaw.provider.request({
    method: "eth_sendTransaction",
    params: [tx],
  });
} catch (error: any) {
  if (error.code === 4001) {
    // User rejected — this is normal, not an error
    return;
  }
  if (error.code === 4100) {
    // Not connected — check auth state BEFORE requests, not after
    await reconnect();
    return;
  }
  console.error("Transaction failed:", error.message);
}

Before any provider request, verify WebAuthn support:

if (!window.PublicKeyCredential) {
  throw new Error("WebAuthn not supported in this browser");
}

Custom UI Handler (AppSpecific Mode)

Implement the UIHandler interface for full control over passkey UI:

import type {
  UIHandler,
  UIHandlerConfig,
  UIRequest,
  UIResponse,
} from "@jaw.id/core";
import { UIError } from "@jaw.id/core";

class CustomUIHandler implements UIHandler {
  private config: UIHandlerConfig | null = null;

  init(config: UIHandlerConfig) {
    this.config = config;
  }

  canHandle(request: UIRequest): boolean {
    return true; // Handle all request types
  }

  async request(request: UIRequest): Promise<UIResponse> {
    // Show your custom modal/UI based on request.method
    // Response ID MUST match request ID — mismatch causes SDK to hang
    const userApproved = await showCustomModal(request);
    if (!userApproved) {
      throw UIError.userRejected();
    }
    return { id: request.id, result: { approved: true } };
  }

  cleanup() {
    // MUST destroy modals, remove listeners, nullify config
    this.config = null;
  }
}

Request types to handle: walletconnect, personalsign, ethsignTypedDatav4, walletsendCalls, walletgrantPermissions, walletrevokePermissions, walletsign.

Key TypeScript Types

// Account configuration
interface AccountConfig {
  chainId: number;
  apiKey: string;
  credentialId?: string;
}

// Transaction calls
interface TransactionCall {
  to: `0x${string}`; // Must have 0x prefix
  value?: bigint; // In wei
  data?: `0x${string}`; // Encoded calldata
}

// Batch operation status
interface CallStatusResponse {
  status: 100 | 200 | 400 | 500; // Pending | Completed | Offchain fail | Onchain revert
}

// Permission types
type SpendPeriod =
  | "minute"
  | "hour"
  | "day"
  | "week"
  | "month"
  | "year"
  | "forever";

interface SpendPermissionDetail {
  token: `0x${string}`;
  allowance: string; // String, not bigint
  period: SpendPeriod;
  multiplier?: number;
}

interface CallPermissionDetail {
  to: `0x${string}`;
  functionSignature?: string; // OR selector, never both
  selector?: `0x${string}`;
}

Configuration Reference

jaw({
  // Required
  apiKey: string,

  // Optional
  appName: string,                    // Default: 'DApp'
  appLogoUrl: string,                 // HTTPS only, min 200x200px
  ens: object,                        // ENS configuration
  defaultChainId: number,             // Initial chain
  paymasters: Record<number, {...}>,  // Per-chain paymaster config
  showTestnets: boolean,              // Required for testnet chains
  preference: {
    mode: Mode.CrossPlatform | Mode.AppSpecific,
    uiHandler: UIHandler,             // Required for AppSpecific
  },
});

Security Considerations

  • Never hardcode API keys — use environment variables (process.env)
  • Spender private keys for permissions must live in a secrets manager or HSM
  • Keep JAW API keys server-side only for headless flows
  • Verify WebAuthn browser support before any passkey operation
  • Never retry after user rejection (code 4001)
  • Check auth state before making requests, not after catching errors
  • Permission revocation is permanent and costs gas — confirm with user first
  • All addresses must have 0x prefix, all values in wei, all timestamps in seconds

References