Integrations · Sign in with Pikeway

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.

Building an MCP client? You may not need any of this by hand. Point it at 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.
1

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.

GET/.well-known/oauth-authorization-server
bash
curl https://gw.pikeway.ai/.well-known/oauth-authorization-server
Response
{
  "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.

2

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.

POST/api/mcp-oauth/register
Body (JSON)
redirect_urisstring[]required
1 to 10 URIs, no fragment. https:// 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_namestring
Shown on the consent page and used to name the user's key (MCP · client_name). Up to 100 characters. Use your product name.
token_endpoint_auth_methodstring
Only none is supported (public client with PKCE). Anything else is rejected with invalid_client_metadata.
bash
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"]
  }'
Response
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"
}
3

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.

GET/oauth/authorize
Query parameters
response_typestringrequired
Always code.
client_idstringrequired
From step 2.
redirect_uristringrequired
One of your registered URIs, exactly.
code_challengestringrequired
base64url(sha256(verifier)) without padding: 43 characters.
code_challenge_methodstringrequired
Always S256; plain is not accepted.
statestring
Random value echoed back on the redirect. Optional in the protocol, but always send one and check it.
javascript
import { 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).

4

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:

Approved
https://acme.example/pikeway/callback?code=SplxlOBeZQQYbYS6WxSbIA…&state=af0ifjsldkj
Denied
https://acme.example/pikeway/callback?state=af0ifjsldkj&error=access_denied
  • Reject the request if state doesn'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.
5

Exchange the code for tokens

Call the token endpoint from your server. It accepts application/x-www-form-urlencoded or JSON.

POST/api/mcp-oauth/token
Body
grant_typestringrequired
authorization_code
codestringrequired
From the callback.
code_verifierstringrequired
The verifier you generated in step 3.
redirect_uristringrequired
Must equal the one used in step 3.
client_idstring
Recommended. If sent, it must match the client the code was issued to.
bash
curl -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
Response
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-…
A Pikeway key owned by the user, limited to the budget they chose. Valid for 30 days.
refresh_tokenpkr-…
Single-use. Exchange it for a new pair before the access token expires.
expires_inseconds
2592000 (30 days).
6

Call 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.

GET/v1/auth/whoami
bash
curl https://gw.pikeway.ai/v1/auth/whoami -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "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.

Spendable balance
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:

Run an 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"}}'
7

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.

Body
grant_typestringrequired
refresh_token
refresh_tokenstringrequired
The latest refresh token you stored.
client_idstring
Recommended; must match if sent.
bash
curl -X POST https://gw.pikeway.ai/api/mcp-oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=pkr-… \
  -d client_id=mcp_Xr4k9pQ2vL8sT1wZb7nYdA3e
Serialize refreshes per user. If two workers refresh with the same token at once, only one succeeds and the other gets invalid_grant. Save the new pair before you use it.
8

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:

WhereResponseMeaning and what to do
API call401 invalid or revoked API keyThe access token expired or the user revoked it. Try a refresh; if that fails, ask them to reconnect.
API call402 api key spend limit reachedYour app used the whole limit the user granted. Ask them to reconnect and pick a higher limit.
API call402 insufficient balanceThe user's Pikeway balance is empty. Link them to /console/billing to top up.
Token endpoint400 invalid_grantCode expired or reused, verifier or redirect_uri mismatch, or the refresh token was revoked or already used. Restart the sign-in flow.
Token endpoint400 invalid_requestcode or code_verifier missing.
Token endpoint400 unsupported_grant_typegrant_type must be authorization_code or refresh_token.

Token endpoint errors use the standard OAuth shape: {"error":"invalid_grant","error_description":"…"}.

Security notes

The access token is a real key that spends the user's money. Keep both tokens on your server, encrypt them at rest, never log them, and never send them to the browser.
  • 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.