Set up Sign in with Pikeway
A standard OAuth 2.1 authorization code flow with PKCE and dynamic client registration. Public clients only: there's no client secret. The access token you get is a budget-limited Pikeway key that belongs to the user.
https://gw.pikeway.ai/mcp/agent: without a token it answers 401 with a WWW-Authenticate: Bearer resource_metadata=… challenge, and MCP SDKs run the whole flow below automatically.Read the server metadata
Every endpoint is published in the authorization server metadata (RFC 8414). The same document is served at /.well-known/openid-configuration for libraries that only look there. Read endpoints from it rather than hard-coding them.
curl https://gw.pikeway.ai/.well-known/oauth-authorization-server
{
"issuer": "https://gw.pikeway.ai",
"authorization_endpoint": "/oauth/authorize",
"token_endpoint": "https://gw.pikeway.ai/api/mcp-oauth/token",
"registration_endpoint": "https://gw.pikeway.ai/api/mcp-oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["pikeway"]
}There's a single scope, pikeway. You can omit scope from requests.
Register your app
Register once (RFC 7591 dynamic client registration; no account or approval needed) and store the client_id in your config. Register again only if your redirect URIs change.
redirect_urisstring[]requiredhttps:// anywhere; http:// only on localhost, 127.0.0.1 or [::1]; custom schemes for desktop apps (myapp://callback), except javascript:, data:, file: and similar. At authorization time the URI must match one of these exactly.client_namestringMCP · client_name). Up to 100 characters. Use your product name.token_endpoint_auth_methodstringnone is supported (public client with PKCE). Anything else is rejected with invalid_client_metadata.curl -X POST https://gw.pikeway.ai/api/mcp-oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Research",
"redirect_uris": ["https://acme.example/pikeway/callback", "http://localhost:3000/pikeway/callback"]
}'HTTP/1.1 201 Created
Cache-Control: no-store
{
"client_id": "mcp_Xr4k9pQ2vL8sT1wZb7nYdA3e",
"client_name": "Acme Research",
"redirect_uris": ["https://acme.example/pikeway/callback", "http://localhost:3000/pikeway/callback"],
"client_id_issued_at": 1792834800,
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Send the user to the consent page
Generate a new PKCE verifier and state for every sign-in, keep them in the user's server-side session, and redirect the browser to the authorization endpoint. PKCE with S256 is required.
response_typestringrequiredcode.client_idstringrequiredredirect_uristringrequiredcode_challengestringrequiredbase64url(sha256(verifier)) without padding: 43 characters.code_challenge_methodstringrequiredS256; plain is not accepted.statestringimport { createHash, randomBytes } from "node:crypto";
// 1. A fresh PKCE verifier and state for every sign-in
const verifier = randomBytes(32).toString("base64url"); // 43 chars
const challenge = createHash("sha256").update(verifier).digest("base64url");
const state = randomBytes(16).toString("base64url");
// Keep verifier + state in the user's server-side session
session.pikeway = { verifier, state };
// 2. Redirect the browser
const url = new URL("/oauth/authorize");
url.search = new URLSearchParams({
response_type: "code",
client_id: process.env.PIKEWAY_CLIENT_ID,
redirect_uri: "https://acme.example/pikeway/callback",
code_challenge: challenge,
code_challenge_method: "S256",
state,
}).toString();
return Response.redirect(url, 302);Users who aren't signed in are sent to the Pikeway login page first and then straight back. The consent page shows your app name, what it can do, the host they'll return to, and a spending limit picker ($1, $5, $20, $100 or no limit; $5 is preselected).
Handle the callback
After the user decides, the browser is sent to your redirect_uri with your state and either a code or an error:
https://acme.example/pikeway/callback?code=SplxlOBeZQQYbYS6WxSbIA…&state=af0ifjsldkj
https://acme.example/pikeway/callback?state=af0ifjsldkj&error=access_denied
- Reject the request if
statedoesn't match what you stored. error=access_denied: the user clicked Deny.error=invalid_request: the PKCE parameters were missing or malformed.- The code is valid for 10 minutes and can be used once. Exchange it right away.
Exchange the code for tokens
Call the token endpoint from your server. It accepts application/x-www-form-urlencoded or JSON.
grant_typestringrequiredauthorization_codecodestringrequiredcode_verifierstringrequiredredirect_uristringrequiredclient_idstringcurl -X POST https://gw.pikeway.ai/api/mcp-oauth/token \ -d grant_type=authorization_code \ -d code=SplxlOBeZQQYbYS6WxSbIA… \ -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk \ -d client_id=mcp_Xr4k9pQ2vL8sT1wZb7nYdA3e \ -d redirect_uri=https://acme.example/pikeway/callback
HTTP/1.1 200 OK
Cache-Control: no-store
{
"access_token": "pk-…",
"token_type": "Bearer",
"expires_in": 2592000,
"refresh_token": "pkr-…",
"scope": "pikeway"
}access_tokenpk-…refresh_tokenpkr-…expires_insecondsCall APIs as the user
Use the access token exactly like any other Pikeway key. Start with /v1/auth/whoami to show who's connected and /v1/wallet/balance to show what they can spend.
curl https://gw.pikeway.ai/v1/auth/whoami -H "Authorization: Bearer $ACCESS_TOKEN"
{
"userId": "cm2x8k1qz0000abcd1234efgh",
"email": "[email protected]",
"walletAddress": null,
"twitterHandle": null,
"key": {
"id": "cm2x8m3rt0001abcd5678ijkl",
"name": "MCP · Acme Research",
"preview": "pk-…9fQ2",
"budget": { "limit": 5, "used": 0.42, "currency": "USD" }
}
}Use userId as the stable identifier to link the Pikeway account to your user. email or walletAddress may be null depending on how the user signed up.
curl https://gw.pikeway.ai/v1/wallet/balance -H "Authorization: Bearer $ACCESS_TOKEN"
# → { "balance": { "value": 18.3, … }, "keyBudget": { "limit": 5, "used": 0.42, "remaining": 4.58, … },
# "spendable": { "value": 4.58, "currency": "USD" } }spendable is the smaller of the user's balance and what's left of the limit they gave you. Then call any endpoint:
curl -X POST https://gw.pikeway.ai/v1/run \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider":"birdeye","endpoint":"/defi/price","input":{"address":"So11111111111111111111111111111111111111112"}}'Refresh before the token expires
Refresh rotates both tokens. You get a new access token and a new refresh token; the old access token stops working immediately and the old refresh token can't be used again. The limit and the amount already spent carry over, so refreshing never resets the user's budget.
grant_typestringrequiredrefresh_tokenrefresh_tokenstringrequiredclient_idstringcurl -X POST https://gw.pikeway.ai/api/mcp-oauth/token \ -d grant_type=refresh_token \ -d refresh_token=pkr-… \ -d client_id=mcp_Xr4k9pQ2vL8sT1wZb7nYdA3e
Handle revocation and limits
Users can revoke your app in Console → Keys at any time, and calls stop when their balance or your limit runs out. Map these responses to clear messages in your UI:
| Where | Response | Meaning and what to do |
|---|---|---|
| API call | 401 invalid or revoked API key | The access token expired or the user revoked it. Try a refresh; if that fails, ask them to reconnect. |
| API call | 402 api key spend limit reached | Your app used the whole limit the user granted. Ask them to reconnect and pick a higher limit. |
| API call | 402 insufficient balance | The user's Pikeway balance is empty. Link them to /console/billing to top up. |
| Token endpoint | 400 invalid_grant | Code expired or reused, verifier or redirect_uri mismatch, or the refresh token was revoked or already used. Restart the sign-in flow. |
| Token endpoint | 400 invalid_request | code or code_verifier missing. |
| Token endpoint | 400 unsupported_grant_type | grant_type must be authorization_code or refresh_token. |
Token endpoint errors use the standard OAuth shape: {"error":"invalid_grant","error_description":"…"}.
Security notes
- Always send and verify state, and generate a fresh PKCE verifier for every sign-in.
- Authorization codes are single-use. If a code is presented twice, Pikeway treats it as stolen and revokes the tokens issued from it.
- Register only redirect URIs you control, and keep the list short. Redirect URIs are matched exactly.
- There's no client secret, so the client_id isn't sensitive. Security comes from PKCE and exact redirect matching.
- To disconnect from your side, delete the stored tokens; the access token expires within 30 days. To revoke it immediately the user removes the key in Console → Keys.