with one click
siwa-magic
Magic server wallet integration for SIWA authentication.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Magic server wallet integration for SIWA authentication.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | siwa-magic |
| version | 0.2.0 |
| description | Magic server wallet integration for SIWA authentication. |
Sign SIWA messages using Magic server-side wallets. Keys are held in a Trusted Execution Environment (TEE) — the agent never touches them.
npm install @buildersgarden/siwa
No additional SDK needed — the Magic signer calls the Magic Express API directly over HTTP.
import { createMagicSiwaSigner } from "@buildersgarden/siwa/signer";
const signer = await createMagicSiwaSigner({
secretKey: process.env.MAGIC_SECRET_KEY!,
jwt: agentJwt, // JWT from your OIDC provider
providerId: process.env.MAGIC_PROVIDER_ID!,
});
The signer calls POST /v1/wallet on creation to fetch (or create) the wallet address. This validates credentials and caches the address for subsequent signing calls.
You need three things:
The JWT maps to an identity in Magic, and each identity has a wallet. No wallet IDs needed — wallet lookup is handled automatically via the JWT.
Magic needs to verify the JWTs your agents present. You register an OIDC-compatible identity provider that tells Magic where to find your public keys.
Option A: Self-signed JWTs (lowest friction)
Generate an RSA key pair and host the public key as a JWKS endpoint. This can be a static file on any public URL (GitHub Gist, your app's /.well-known/jwks.json route, S3, etc.).
import { generateKeyPair, exportJWK, exportPKCS8 } from "jose";
const { publicKey, privateKey } = await generateKeyPair("RS256", {
extractable: true,
});
// Save private key (keep secret — used to sign JWTs)
const privatePem = await exportPKCS8(privateKey);
// Build JWKS (host publicly)
const jwk = await exportJWK(publicKey);
jwk.kid = "my-agent-key-1";
jwk.alg = "RS256";
jwk.use = "sig";
const jwks = { keys: [jwk] };
Then mint JWTs with matching issuer and audience claims:
import { SignJWT, importPKCS8 } from "jose";
const key = await importPKCS8(privatePem, "RS256");
const jwt = await new SignJWT({ sub: "my-agent" })
.setProtectedHeader({ alg: "RS256", kid: "my-agent-key-1" })
.setIssuedAt()
.setExpirationTime("1h")
.setIssuer("my-app")
.setAudience("my-app")
.sign(key);
Option B: External identity provider
Use any OIDC-compatible provider (Auth0, Clerk, Firebase Auth, etc.) — just point Magic to their JWKS endpoint.
curl -X POST 'https://tee.express.magiclabs.com/v1/identity/provider' \
-H 'Content-Type: application/json' \
-H 'X-Magic-Secret-Key: YOUR_SECRET_KEY' \
-d '{
"issuer": "my-app",
"audience": "my-app",
"jwks_uri": "https://example.com/.well-known/jwks.json"
}'
The response contains the provider_id — use this as MAGIC_PROVIDER_ID.
Important: The issuer and audience must match the iss and aud claims in your JWTs. The jwks_uri must be publicly reachable so Magic can fetch the public keys for verification.
If your agent doesn't have an ERC-8004 identity yet, register onchain. You'll need a funded wallet (Base Sepolia ETH for testnet).
import { encodeFunctionData } from "viem";
const IDENTITY_REGISTRY_ADDRESS = "0x8004A818BFB912233c491871b3d84c89A494BD9e";
const BASE_SEPOLIA_CAIP2 = "eip155:84532";
const IDENTITY_REGISTRY_ABI = [
{
name: "register",
type: "function",
inputs: [{ name: "agentURI", type: "string" }],
outputs: [{ name: "agentId", type: "uint256" }],
},
] as const;
const metadata = {
name: "My Agent",
description: "A helpful AI assistant",
capabilities: ["chat", "analysis"],
};
const agentURI = `data:application/json;base64,${Buffer.from(JSON.stringify(metadata)).toString("base64")}`;
const data = encodeFunctionData({
abi: IDENTITY_REGISTRY_ABI,
functionName: "register",
args: [agentURI],
});
// Sign and broadcast the registration transaction
const { createPublicClient, http } = await import("viem");
const { baseSepolia } = await import("viem/chains");
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
const address = await signer.getAddress();
const nonce = await publicClient.getTransactionCount({ address });
const gas = await publicClient.estimateGas({ account: address, to: IDENTITY_REGISTRY_ADDRESS, data });
const fees = await publicClient.estimateFeesPerGas();
const signedTx = await signer.signTransaction({
to: IDENTITY_REGISTRY_ADDRESS,
data,
nonce,
chainId: 84532,
gas,
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
});
const hash = await publicClient.sendRawTransaction({ serializedTransaction: signedTx });
The authentication flow consists of two steps:
Note: The URLs below (
api.example.com) are placeholders. Replace them with your own server that implements the SIWA verification endpoints. See the Server-Side Verification skill for implementation details.
/siwa/nonce endpoint/siwa/verifyconst nonceRes = await fetch("https://api.example.com/siwa/nonce", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
address: await signer.getAddress(),
agentId: 42,
agentRegistry: "eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e",
}),
});
const { nonce, nonceToken, issuedAt, expirationTime } = await nonceRes.json();
import { signSIWAMessage } from "@buildersgarden/siwa";
const { message, signature, address } = await signSIWAMessage({
domain: "api.example.com",
uri: "https://api.example.com/siwa",
agentId: 42,
agentRegistry: "eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e",
chainId: 84532,
nonce,
issuedAt,
expirationTime,
}, signer);
// Send to server for verification
const verifyRes = await fetch("https://api.example.com/siwa/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, signature, nonceToken }),
});
const { receipt, agentId } = await verifyRes.json();
// Store the receipt for authenticated API calls
import { signAuthenticatedRequest } from "@buildersgarden/siwa/erc8128";
const request = new Request("https://api.example.com/action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "execute" }),
});
const signedRequest = await signAuthenticatedRequest(
request,
receipt, // from SIWA sign-in
signer,
84532,
);
const response = await fetch(signedRequest);
Agent (using SIWA SDK)
│
├─ createMagicSiwaSigner({ jwt, providerId })
│ └─ POST /v1/wallet → gets wallet address
│
├─ signSIWAMessage(fields, signer)
│ └─ signer.signMessage(message)
│ └─ POST /v1/wallet/sign/message → TEE signs, returns signature
│
├─ signer.signTransaction(tx)
│ └─ POST /v1/wallet/sign/data → TEE signs tx hash, returns r/s/v
│ → caller broadcasts signed tx via any RPC provider
│
└─ sends { message, signature } to verifying service
The Magic Express API proxies all signing to a Trusted Execution Environment. Private keys never leave the TEE, and the agent never has direct access to them.
MAGIC_SECRET_KEY=sk-live-... # From your Magic dashboard
MAGIC_PROVIDER_ID=your-provider-id # From POST /v1/identity/provider (see Prerequisites)
The JWT is passed per-request (not an env var) since each agent has its own token.
SIWA (Sign-In With Agent) authentication for ERC-8004 registered agents.
Openfort backend wallet integration for SIWA authentication.
Use this skill to implement server-side SIWA verification. For backends and APIs that need to authenticate ERC-8004 agents without signing capabilities.
Bankr wallet integration for SIWA authentication.
Circle developer-controlled wallet integration for SIWA authentication.
Self-hosted keyring proxy signer with optional 2FA for secure key isolation.