[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71K74T95HBKARFAR274MKB9":3,"skill-same:01M71K74T95HBKARFAR274MKB9":88},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":64,"commitSha":65,"skillDirectory":66,"forksCount":67,"repositoryHtmlUrl":68,"repositoryHomepageUrl":69,"ownerHtmlUrl":70,"ownerBlogUrl":71,"ownerDescription":72,"updatedAt":73,"install":74,"resources":87},"01M71K74T95HBKARFAR274MKB9","openrouter-oauth",null,"Implement \"Sign In with OpenRouter\" using OAuth PKCE — framework-agnostic, no SDK or client registration required. Use when the user wants to add OpenRouter login, authentication, sign-in buttons, OAuth, or AI model inference API keys for browser-based apps. No client registration, no backend, no secrets required.","clawdsolana\u002FOpenClawd","clawdsolana","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F279036008?v=4","01M71JTVF9FNGKE2SVJRPTQM31","OpenClawd","01M71JTVGCKWBSS8ZYC1HYWVA5",2,"MIT","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fopen-router-skills\u002Fopenrouter-oauth\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"openrouter oauth","OAuth","openrouter","for","registration","framework","add","ui","secrets","login","buttons","localstorage","browser","api","when","apps","authentication","api keys","auth","model","2026-11-10 10:12:30",false,"---\nname: openrouter-oauth\ndescription: Implement \"Sign In with OpenRouter\" using OAuth PKCE — framework-agnostic, no SDK or client registration required. Use when the user wants to add OpenRouter login, authentication, sign-in buttons, OAuth, or AI model inference API keys for browser-based apps. No client registration, no backend, no secrets required.\nversion: 2.0.0\ncompatibility: browser (requires Web Crypto API, localStorage, sessionStorage)\nmetadata:\n  solanaos:\n    emoji: \"🔑\"\n    registryKey: \"openrouter.oauth\"\n    kind: \"auth\"\n    requires:\n      bins: []\n      env: []\n    injectAt: \"ui-bootstrap\"\n---\n\n# Sign In with OpenRouter\n\nAdd OAuth login to any web app. Users authorize on OpenRouter and your app receives an API key — no client registration, no backend, no secrets. Works with any framework.\n\nLive demo: [openrouterteam.github.io\u002Fsign-in-with-openrouter](https:\u002F\u002Fopenrouterteam.github.io\u002Fsign-in-with-openrouter\u002F)\n\n## Decision Tree\n\n| User wants to… | Do this |\n|---|---|\n| Add sign-in \u002F login to a web app | Follow the full PKCE flow + button guidance below |\n| Get an API key programmatically (no UI) | Just implement the PKCE flow — skip the button section |\n| Use the OpenRouter SDK after auth | Do PKCE here for the key, then see `openrouter-typescript-sdk` skill for `callModel`\u002Fstreaming |\n\n---\n\n## OAuth PKCE Flow\n\nNo client ID or secret — the PKCE challenge is the only proof of identity.\n\n### Step 1: Generate verifier and challenge\n\n```\ncode_verifier  = base64url(32 random bytes)\ncode_challenge = base64url(SHA-256(code_verifier))\n```\n\n- Use `crypto.getRandomValues(new Uint8Array(32))` for the random bytes\n- base64url encoding: standard base64, then replace `+` → `-`, `\u002F` → `_`, strip trailing `=`\n- Store `code_verifier` in **`sessionStorage`** (not `localStorage`) — so the verifier doesn't persist after the tab closes or leak to other tabs (security: the verifier is a one-time secret)\n\n### Step 2: Redirect to OpenRouter\n\n```\nhttps:\u002F\u002Fopenrouter.ai\u002Fauth?callback_url={url}&code_challenge={challenge}&code_challenge_method=S256\n```\n\n| Param | Value |\n|---|---|\n| `callback_url` | Your app's URL (where the user returns after auth) |\n| `code_challenge` | The S256 challenge from Step 1 |\n| `code_challenge_method` | Always `S256` |\n\n### Step 3: Handle the redirect back\n\nUser returns to your `callback_url` with `?code=` appended. Extract the `code` query parameter.\n\n**Important:** Before processing `?code=`, check that a `code_verifier` exists in `sessionStorage`. Other routes or third-party code might use `?code=` query params for unrelated purposes — a `hasOAuthCallbackPending()` guard ensures you only consume codes that belong to your OAuth flow.\n\n### Step 4: Exchange code for API key\n\n```\nPOST https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fauth\u002Fkeys\nContent-Type: application\u002Fjson\n\n{\n  \"code\": \"\u003Ccode from query param>\",\n  \"code_verifier\": \"\u003Cverifier from sessionStorage>\",\n  \"code_challenge_method\": \"S256\"\n}\n\n→ { \"key\": \"sk-or-...\" }\n```\n\nRemove the verifier from `sessionStorage` before or after the exchange.\n\n### Step 5: Store the key and clean up\n\n- Store `key` in `localStorage`\n- Clean the URL: `history.replaceState({}, \"\", location.pathname)` to remove `?code=`\n- **Cross-tab sync:** Listen for `storage` events on the API key's `localStorage` entry so other tabs update when the user signs in or out\n\n---\n\n## Auth Module Reference\n\nDrop-in module implementing the full PKCE flow. Reduces risk of getting base64url encoding, sessionStorage handling, or the key exchange wrong.\n\n```typescript\n\u002F\u002F lib\u002Fopenrouter-auth.ts\nconst STORAGE_KEY = \"openrouter_api_key\";\nconst VERIFIER_KEY = \"openrouter_code_verifier\";\n\ntype AuthListener = () => void;\nconst listeners = new Set\u003CAuthListener>();\nexport const onAuthChange = (fn: AuthListener) => { listeners.add(fn); return () => listeners.delete(fn); };\nconst notify = () => listeners.forEach((fn) => fn());\n\n\u002F\u002F Cross-tab sync: other tabs update when user signs in\u002Fout\nif (typeof window !== \"undefined\") {\n  window.addEventListener(\"storage\", (e) => { if (e.key === STORAGE_KEY) notify(); });\n}\n\nexport const getApiKey = (): string | null =>\n  typeof window !== \"undefined\" ? localStorage.getItem(STORAGE_KEY) : null;\n\nexport const setApiKey = (key: string) => { localStorage.setItem(STORAGE_KEY, key); notify(); };\nexport const clearApiKey = () => { localStorage.removeItem(STORAGE_KEY); notify(); };\n\n\u002F\u002F Guard: only process ?code= if we initiated an OAuth flow in this tab\nexport const hasOAuthCallbackPending = (): boolean =>\n  typeof window !== \"undefined\" && sessionStorage.getItem(VERIFIER_KEY) !== null;\n\nfunction generateCodeVerifier(): string {\n  const bytes = new Uint8Array(32);\n  crypto.getRandomValues(bytes);\n  return btoa(String.fromCharCode(...bytes))\n    .replace(\u002F\\+\u002Fg, \"-\").replace(\u002F\\\u002F\u002Fg, \"_\").replace(\u002F=+$\u002F, \"\");\n}\n\nasync function computeS256Challenge(verifier: string): Promise\u003Cstring> {\n  const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n  return btoa(String.fromCharCode(...new Uint8Array(digest)))\n    .replace(\u002F\\+\u002Fg, \"-\").replace(\u002F\\\u002F\u002Fg, \"_\").replace(\u002F=+$\u002F, \"\");\n}\n\nexport async function initiateOAuth(callbackUrl?: string): Promise\u003Cvoid> {\n  const verifier = generateCodeVerifier();\n  sessionStorage.setItem(VERIFIER_KEY, verifier);\n  const challenge = await computeS256Challenge(verifier);\n  const url = callbackUrl ?? window.location.origin + window.location.pathname;\n  window.location.href = `https:\u002F\u002Fopenrouter.ai\u002Fauth?${new URLSearchParams({\n    callback_url: url, code_challenge: challenge, code_challenge_method: \"S256\",\n  })}`;\n}\n\nexport async function handleOAuthCallback(code: string): Promise\u003Cvoid> {\n  const verifier = sessionStorage.getItem(VERIFIER_KEY);\n  if (!verifier) throw new Error(\"Missing code verifier\");\n  sessionStorage.removeItem(VERIFIER_KEY);\n  const res = await fetch(\"https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fauth\u002Fkeys\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application\u002Fjson\" },\n    body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: \"S256\" }),\n  });\n  if (!res.ok) throw new Error(`Key exchange failed (${res.status})`);\n  const { key } = await res.json();\n  setApiKey(key);\n}\n```\n\n---\n\n## Sign-in Button\n\nBuild a button component that calls `initiateOAuth()` on click. Include the OpenRouter logo and provide multiple visual variants.\n\n### OpenRouter Logo SVG\n\n```svg\n\u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" stroke=\"currentColor\">\n  \u003Cpath d=\"M3 248.945C18 248.945 76 236 106 219C136 202 136 202 198 158C276.497 102.293 332 120.945 423 120.945\" stroke-width=\"90\"\u002F>\n  \u003Cpath d=\"M511 121.5L357.25 210.268L357.25 32.7324L511 121.5Z\"\u002F>\n  \u003Cpath d=\"M0 249C15 249 73 261.945 103 278.945C133 295.945 133 295.945 195 339.945C273.497 395.652 329 377 420 377\" stroke-width=\"90\"\u002F>\n  \u003Cpath d=\"M508 376.445L354.25 287.678L354.25 465.213L508 376.445Z\"\u002F>\n\u003C\u002Fsvg>\n```\n\n### Variants (Tailwind)\n\nRecommended classes for visual consistency with the reference implementation:\n\n| Variant | Classes |\n|---|---|\n| `default` | `rounded-lg border border-neutral-300 bg-white text-neutral-900 shadow-sm hover:bg-neutral-50` |\n| `minimal` | `text-neutral-700 underline-offset-4 hover:underline` |\n| `branded` | `rounded-lg bg-neutral-900 text-white shadow hover:bg-neutral-800` |\n| `icon` | Same as `default` + `aspect-square` (logo only, no text) |\n| `cta` | `rounded-xl bg-neutral-900 text-white shadow-lg hover:bg-neutral-800 hover:scale-[1.02] active:scale-[0.98]` |\n\n### Sizes\n\n| Size | Classes |\n|---|---|\n| `sm` | `h-8 px-3 text-xs` |\n| `default` | `h-10 px-5 text-sm` |\n| `lg` | `h-12 px-8 text-base` |\n| `xl` | `h-14 px-10 text-lg` |\n\nAll variants use: `inline-flex items-center justify-center gap-2 font-medium transition-all cursor-pointer disabled:opacity-50`\n\nShow a loading indicator while the key exchange is in progress. Default label: \"Sign in with OpenRouter\".\n\n### Dark mode\n\nFor dark mode support, add dark variants: swap light backgrounds to dark (`dark:bg-neutral-900 dark:text-white`) and vice versa for `branded`\u002F`cta` (`dark:bg-white dark:text-neutral-900`).\n\n---\n\n## Using the API Key\n\n```typescript\nconst response = await fetch(\"https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fresponses\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${apiKey}`,\n    \"Content-Type\": \"application\u002Fjson\",\n  },\n  body: JSON.stringify({\n    model: \"openai\u002Fgpt-4o-mini\",\n    input: [{ type: \"message\", role: \"user\", content: \"Hello!\" }],\n  }),\n});\n```\n\nFor the type-safe SDK approach (`callModel`, streaming, tool use), see the `openrouter-typescript-sdk` skill.\n\n---\n\n## Resources\n\n- [OAuth PKCE guide](https:\u002F\u002Fopenrouter.ai\u002Fdocs\u002Fguides\u002Foverview\u002Fauth\u002Foauth) — full parameter reference and key management\n- [Authentication guide](https:\u002F\u002Fopenrouter.ai\u002Fdocs\u002Fapi\u002Freference\u002Fauthentication) — API key usage and Bearer token setup\n- [Live demo](https:\u002F\u002Fopenrouterteam.github.io\u002Fsign-in-with-openrouter\u002F) — interactive button playground\n- [OpenRouter TypeScript SDK](https:\u002F\u002Fopenrouter.ai\u002Fdocs\u002Fsdks\u002Ftypescript\u002Foverview) — `callModel` pattern for completions and streaming\n","# Sign In with OpenRouter\n\nAdd OAuth login to any web app. Users authorize on OpenRouter and your app receives an API key — no client registration, no backend, no secrets. Works with any framework.\n\nLive demo: [openrouterteam.github.io\u002Fsign-in-with-openrouter](https:\u002F\u002Fopenrouterteam.github.io\u002Fsign-in-with-openrouter\u002F)\n\n## Decision Tree\n\n| User wants to… | Do this |\n|---|---|\n| Add sign-in \u002F login to a web app | Follow the full PKCE flow + button guidance below |\n| Get an API key programmatically (no UI) | Just implement the PKCE flow — skip the button section |\n| Use the OpenRouter SDK after auth | Do PKCE here for the key, then see `openrouter-typescript-sdk` skill for `callModel`\u002Fstreaming |\n\n---\n\n## OAuth PKCE Flow\n\nNo client ID or secret — the PKCE challenge is the only proof of identity.\n\n### Step 1: Generate verifier and challenge\n\n```\ncode_verifier  = base64url(32 random bytes)\ncode_challenge = base64url(SHA-256(code_verifier))\n```\n\n- Use `crypto.getRandomValues(new Uint8Array(32))` for the random bytes\n- base64url encoding: standard base64, then replace `+` → `-`, `\u002F` → `_`, strip trailing `=`\n- Store `code_verifier` in **`sessionStorage`** (not `localStorage`) — so the verifier doesn't persist after the tab closes or leak to other tabs (security: the verifier is a one-time secret)\n\n### Step 2: Redirect to OpenRouter\n\n```\nhttps:\u002F\u002Fopenrouter.ai\u002Fauth?callback_url={url}&code_challenge={challenge}&code_challenge_method=S256\n```\n\n| Param | Value |\n|---|---|\n| `callback_url` | Your app's URL (where the user returns after auth) |\n| `code_challenge` | The S256 challenge from Step 1 |\n| `code_challenge_method` | Always `S256` |\n\n### Step 3: Handle the redirect back\n\nUser returns to your `callback_url` with `?code=` appended. Extract the `code` query parameter.\n\n**Important:** Before processing `?code=`, check that a `code_verifier` exists in `sessionStorage`. Other routes or third-party code might use `?code=` query params for unrelated purposes — a `hasOAuthCallbackPending()` guard ensures you only consume codes that belong to your OAuth flow.\n\n### Step 4: Exchange code for API key\n\n```\nPOST https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fauth\u002Fkeys\nContent-Type: application\u002Fjson\n\n{\n  \"code\": \"\u003Ccode from query param>\",\n  \"code_verifier\": \"\u003Cverifier from sessionStorage>\",\n  \"code_challenge_method\": \"S256\"\n}\n\n→ { \"key\": \"sk-or-...\" }\n```\n\nRemove the verifier from `sessionStorage` before or after the exchange.\n\n### Step 5: Store the key and clean up\n\n- Store `key` in `localStorage`\n- Clean the URL: `history.replaceState({}, \"\", location.pathname)` to remove `?code=`\n- **Cross-tab sync:** Listen for `storage` events on the API key's `localStorage` entry so other tabs update when the user signs in or out\n\n---\n\n## Auth Module Reference\n\nDrop-in module implementing the full PKCE flow. Reduces risk of getting base64url encoding, sessionStorage handling, or the key exchange wrong.\n\n```typescript\n\u002F\u002F lib\u002Fopenrouter-auth.ts\nconst STORAGE_KEY = \"openrouter_api_key\";\nconst VERIFIER_KEY = \"openrouter_code_verifier\";\n\ntype AuthListener = () => void;\nconst listeners = new Set\u003CAuthListener>();\nexport const onAuthChange = (fn: AuthListener) => { listeners.add(fn); return () => listeners.delete(fn); };\nconst notify = () => listeners.forEach((fn) => fn());\n\n\u002F\u002F Cross-tab sync: other tabs update when user signs in\u002Fout\nif (typeof window !== \"undefined\") {\n  window.addEventListener(\"storage\", (e) => { if (e.key === STORAGE_KEY) notify(); });\n}\n\nexport const getApiKey = (): string | null =>\n  typeof window !== \"undefined\" ? localStorage.getItem(STORAGE_KEY) : null;\n\nexport const setApiKey = (key: string) => { localStorage.setItem(STORAGE_KEY, key); notify(); };\nexport const clearApiKey = () => { localStorage.removeItem(STORAGE_KEY); notify(); };\n\n\u002F\u002F Guard: only process ?code= if we initiated an OAuth flow in this tab\nexport const hasOAuthCallbackPending = (): boolean =>\n  typeof window !== \"undefined\" && sessionStorage.getItem(VERIFIER_KEY) !== null;\n\nfunction generateCodeVerifier(): string {\n  const bytes = new Uint8Array(32);\n  crypto.getRandomValues(bytes);\n  return btoa(String.fromCharCode(...bytes))\n    .replace(\u002F\\+\u002Fg, \"-\").replace(\u002F\\\u002F\u002Fg, \"_\").replace(\u002F=+$\u002F, \"\");\n}\n\nasync function computeS256Challenge(verifier: string): Promise\u003Cstring> {\n  const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n  return btoa(String.fromCharCode(...new Uint8Array(digest)))\n    .replace(\u002F\\+\u002Fg, \"-\").replace(\u002F\\\u002F\u002Fg, \"_\").replace(\u002F=+$\u002F, \"\");\n}\n\nexport async function initiateOAuth(callbackUrl?: string): Promise\u003Cvoid> {\n  const verifier = generateCodeVerifier();\n  sessionStorage.setItem(VERIFIER_KEY, verifier);\n  const challenge = await computeS256Challenge(verifier);\n  const url = callbackUrl ?? window.location.origin + window.location.pathname;\n  window.location.href = `https:\u002F\u002Fopenrouter.ai\u002Fauth?${new URLSearchParams({\n    callback_url: url, code_challenge: challenge, code_challenge_method: \"S256\",\n  })}`;\n}\n\nexport async function handleOAuthCallback(code: string): Promise\u003Cvoid> {\n  const verifier = sessionStorage.getItem(VERIFIER_KEY);\n  if (!verifier) throw new Error(\"Missing code verifier\");\n  sessionStorage.removeItem(VERIFIER_KEY);\n  const res = await fetch(\"https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fauth\u002Fkeys\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application\u002Fjson\" },\n    body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: \"S256\" }),\n  });\n  if (!res.ok) throw new Error(`Key exchange failed (${res.status})`);\n  const { key } = await res.json();\n  setApiKey(key);\n}\n```\n\n---\n\n## Sign-in Button\n\nBuild a button component that calls `initiateOAuth()` on click. Include the OpenRouter logo and provide multiple visual variants.\n\n### OpenRouter Logo SVG\n\n```svg\n\u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" stroke=\"currentColor\">\n  \u003Cpath d=\"M3 248.945C18 248.945 76 236 106 219C136 202 136 202 198 158C276.497 102.293 332 120.945 423 120.945\" stroke-width=\"90\"\u002F>\n  \u003Cpath d=\"M511 121.5L357.25 210.268L357.25 32.7324L511 121.5Z\"\u002F>\n  \u003Cpath d=\"M0 249C15 249 73 261.945 103 278.945C133 295.945 133 295.945 195 339.945C273.497 395.652 329 377 420 377\" stroke-width=\"90\"\u002F>\n  \u003Cpath d=\"M508 376.445L354.25 287.678L354.25 465.213L508 376.445Z\"\u002F>\n\u003C\u002Fsvg>\n```\n\n### Variants (Tailwind)\n\nRecommended classes for visual consistency with the reference implementation:\n\n| Variant | Classes |\n|---|---|\n| `default` | `rounded-lg border border-neutral-300 bg-white text-neutral-900 shadow-sm hover:bg-neutral-50` |\n| `minimal` | `text-neutral-700 underline-offset-4 hover:underline` |\n| `branded` | `rounded-lg bg-neutral-900 text-white shadow hover:bg-neutral-800` |\n| `icon` | Same as `default` + `aspect-square` (logo only, no text) |\n| `cta` | `rounded-xl bg-neutral-900 text-white shadow-lg hover:bg-neutral-800 hover:scale-[1.02] active:scale-[0.98]` |\n\n### Sizes\n\n| Size | Classes |\n|---|---|\n| `sm` | `h-8 px-3 text-xs` |\n| `default` | `h-10 px-5 text-sm` |\n| `lg` | `h-12 px-8 text-base` |\n| `xl` | `h-14 px-10 text-lg` |\n\nAll variants use: `inline-flex items-center justify-center gap-2 font-medium transition-all cursor-pointer disabled:opacity-50`\n\nShow a loading indicator while the key exchange is in progress. Default label: \"Sign in with OpenRouter\".\n\n### Dark mode\n\nFor dark mode support, add dark variants: swap light backgrounds to dark (`dark:bg-neutral-900 dark:text-white`) and vice versa for `branded`\u002F`cta` (`dark:bg-white dark:text-neutral-900`).\n\n---\n\n## Using the API Key\n\n```typescript\nconst response = await fetch(\"https:\u002F\u002Fopenrouter.ai\u002Fapi\u002Fv1\u002Fresponses\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${apiKey}`,\n    \"Content-Type\": \"application\u002Fjson\",\n  },\n  body: JSON.stringify({\n    model: \"openai\u002Fgpt-4o-mini\",\n    input: [{ type: \"message\", role: \"user\", content: \"Hello!\" }],\n  }),\n});\n```\n\nFor the type-safe SDK approach (`callModel`, streaming, tool use), see the `openrouter-typescript-sdk` skill.\n\n---\n\n## Resources\n\n- [OAuth PKCE guide](https:\u002F\u002Fopenrouter.ai\u002Fdocs\u002Fguides\u002Foverview\u002Fauth\u002Foauth) — full parameter reference and key management\n- [Authentication guide](https:\u002F\u002Fopenrouter.ai\u002Fdocs\u002Fapi\u002Freference\u002Fauthentication) — API key usage and Bearer token setup\n- [Live demo](https:\u002F\u002Fopenrouterteam.github.io\u002Fsign-in-with-openrouter\u002F) — interactive button playground\n- [OpenRouter TypeScript SDK](https:\u002F\u002Fopenrouter.ai\u002Fdocs\u002Fsdks\u002Ftypescript\u002Foverview) — `callModel` pattern for completions and streaming\n",{"name":5,"version":54,"metadata":55,"description":7,"compatibility":63},"2.0.0",{"solanaos":56},{"kind":47,"emoji":57,"injectAt":58,"requires":59,"registryKey":62},"🔑","ui-bootstrap",{"env":60,"bins":61},[],[],"openrouter.oauth","browser (requires Web Crypto API, localStorage, sessionStorage)","main","6b4a291df75c639681e93b4b5b812126443fd6ed","open-router-skills\u002Fopenrouter-oauth",3,"https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd","https:\u002F\u002Fsolanaclawd.com\u002Fopenclawd","https:\u002F\u002Fgithub.com\u002Fclawdsolana","https:\u002F\u002Fmusebook.trade","Welcome to Open Clawd. The future of Ai on and off chain. Bringing webMCP to web 3 using X402, A2A, MPP and so much more!","2026-08-01 09:30:51",{"source":8,"skillName":5,"commands":75,"prompt":85,"zipAvailable":86},[76,79,82],{"tool":77,"command":78},"npx","npx skills add clawdsolana\u002FOpenClawd --skill openrouter-oauth",{"tool":80,"command":81},"bunx","bunx skills add clawdsolana\u002FOpenClawd --skill openrouter-oauth",{"tool":83,"command":84},"pnpm","pnpm dlx skills add clawdsolana\u002FOpenClawd --skill openrouter-oauth","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fopen-router-skills\u002Fopenrouter-oauth\u002FSKILL.md",true,[],[89,117,143,169,179,203],{"skillNo":90,"name":91,"nameZh":6,"description":92,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":98,"publishedAt":116},"01M71KRBD0W01EAXJ8YM4M1SJ2","solana-dev","End-to-end Solana development playbook (Jan 2026). Prefer Solana Foundation framework-kit (@solana\u002Fclient + @solana\u002Freact-hooks) for React\u002FNext.js UI. Prefer @solana\u002Fkit for all new client\u002FRPC\u002Ftransaction code. When legacy dependencies require web3.js, isolate it behind @solana\u002Fweb3-compat (or @solana\u002Fweb3.js as a true legacy fallback). Covers wallet-standard-first connection (incl. ConnectorKit), Anchor\u002FPinocchio programs, Codama-based client generation, LiteSVM\u002FMollusk\u002FSurfpool testing, and security checklists.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fsrc\u002Fskills\u002Fsolana-dev\u002FSKILL.md",[95],{"categoryNo":96,"categoryName":97,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",[99,100,101,102,103,104,105,106,107,43,108,109,110,111,112,113,114,36,115,34],"solana dev","dev","solana","Wallet","end","testing","hooks","kit","new","web3","code","react","next","2026","playbook","security","fallback","2026-11-10 10:56:23",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":126,"publishedAt":116},"01M71KR8QPESSY61SVE8ZTFNVT","xurl","A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fxurl\u002FSKILL.md",[123],{"categoryNo":124,"categoryName":125,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[119,127,128,129,130,131,132,43,133,42,134,135,136,137,138,139,32,140,141,142],"posts","Media","x api","search","send","tap","you","cli","read","install","cli tool","endpoint","brew","manage","tool","twitter",{"skillNo":144,"name":145,"nameZh":6,"description":146,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":147,"categories":148,"keywords":150,"publishedAt":116},"01M71KR6FSXV88YDKWP7NBY3SH","wurk-integration","WURK API integration for monetizing agents with x402 payments on Solana\u002FBase - create social jobs, custom challenges, and agent help tasks","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fwurk-integration\u002FSKILL.md",[149],{"categoryNo":124,"categoryName":125,"taxonomyType":21,"taxonomyTypeDesc":22},[151,152,153,154,155,156,157,158,42,159,160,161,162,163,164,165,32,166,167,168],"wurk","wurk integration","integration","x402","social","payments","tasks","team","create","social-jobs","command","agent-help","custom-jobs","clawd","safe","api integration","custom","true",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":175,"publishedAt":116},"01M71KR4HCX41BPAK9GP7RPRRP","workflows","Manage application workflows including configuration, restart, and removal.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fworkflows\u002FSKILL.md",[],[171,140,176,177,178],"configuration","application","restart",{"skillNo":180,"name":181,"nameZh":6,"description":182,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":183,"categories":184,"keywords":185,"publishedAt":116},"01M71KR1VM0F5GPEQDNG5J7A0H","web-search","Search the web, fetch content, extract branding profiles, and capture screenshots from URLs. Use for real-time information, API documentation, current events, design matching, and visual reference.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fweb-search\u002FSKILL.md",[],[186,130,187,188,189,32,190,191,192,193,194,195,196,42,197,198,199,200,201,202],"web search","web","fetch","api documentation","real-time","time","profiles","design","use","information","visual","documentation","branding","matching","reference","capture","content",{"skillNo":204,"name":205,"nameZh":6,"description":206,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":207,"categories":208,"keywords":209,"publishedAt":116},"01M71KQZN5MR1JFQHG5GWXYAHS","weather","Get current weather and forecasts via wttr.in or Open-Meteo. Use when: user asks about weather, temperature, or forecasts for any location. NOT for: historical weather data, severe weather alerts, or detailed meteorological analysis. No API key needed.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fweather\u002FSKILL.md",[],[205,210,32,211,194,43,212,42,213,214,215,216,217,218,219,220,221,222],"get","open","analysis","alerts","temperature","curl","via","help","about","https","location","data","user"]