GraphQL API#
PortalPay exposes a GraphQL endpoint for read-only queries over user presence and leaderboards. All developer requests require an Azure API Management (APIM) subscription key. The APIM custom domain is the primary client endpoint; Azure Front Door (AFD) can be configured as an optional/fallback edge.
- Base URL: markup
https://api.pay.ledger1.ai/portalpay - Authentication (Developer APIs): APIM subscription key in header
- markup
Ocp-Apim-Subscription-Key: {your-subscription-key}
- Gateway posture: APIM custom domain is primary. If AFD is enabled, APIM accepts an internal markupheader per policy.
x-edge-secret - Rate limit headers (if enabled): markup,
X-RateLimit-Limitmarkup,X-RateLimit-RemainingmarkupX-RateLimit-Reset
Endpoint#
- GET/POST markup
/portalpay/api/graphql- Prefer POST with markup
Content-Type: application/json - GET is supported for simple queries (URL length limits apply)
- Prefer POST with
Headers:
markupOcp-Apim-Subscription-Key: {your-subscription-key} Content-Type: application/json
Notes:
- Only read queries are available via APIM developer subscriptions.
- Mutations (e.g., markup,
upsertUsermarkup,setPresencemarkup) are not available via APIM developer subscriptions and must be performed within the PortalPay Admin UI where JWT cookies and CSRF protections apply.follow - Do not send wallet identity headers; APIM resolves identity from your subscription and strips wallet headers. Provide viewer context as GraphQL variables when needed.
Schema (Read-Only Queries)#
Available queries (high level):
- markup— Fetch a user profile by wallet address
user(wallet: ID!): User - markup— Followers/following counts and whether
follows(wallet: ID!, viewer: ID): FollowsInfo!markupfollowsviewermarkupwallet - markup— Users considered currently live and public
liveUsers: [LiveUser!]! - markup— Top users ranked by XP (max 200)
leaderboard(limit: Int = 50): [User!]!
Important return types:
- markupfields include:
Usermarkup,walletmarkup,displayNamemarkup,biomarkup,pfpUrlmarkup,xpmarkup,followersCountmarkup,followingCountmarkup,livemarkup,liveSincemarkup,lastHeartbeatmarkup,spaceUrlmarkupspacePublic - markupfields include:
LiveUsermarkup,walletmarkup,displayNamemarkup,pfpUrlmarkup,spaceUrlmarkup,liveSincemarkuplastHeartbeat - markupfields:
FollowsInfomarkup,followersCountmarkup,followingCountmarkupviewerFollows
Example Requests#
# Example: fetch a user and follows info, plus live users and a leaderboard
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/graphql" \
-H "Content-Type: application/json" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
-d '{
"query": "query($wallet: ID!, $viewer: ID) { user(wallet: $wallet) { wallet displayName pfpUrl xp live liveSince lastHeartbeat spaceUrl spacePublic followersCount followingCount } follows(wallet: $wallet, viewer: $viewer) { followersCount followingCount viewerFollows } liveUsers { wallet displayName pfpUrl spaceUrl liveSince lastHeartbeat } leaderboard(limit: 10) { wallet displayName pfpUrl xp } }",
"variables": { "wallet": "0x1234567890abcdef1234567890abcdef12345678", "viewer": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" }
}'POST/portalpay/api/graphqlTry It: GraphQL Read Queries
Run read-only queries.
Default is the APIM custom domain. For AFD, enter only the AFD endpoint host (e.g., https://afd-endpoint-pay-...) without any path; the /portalpay prefix is added automatically for /api/* and /healthz.
The key is kept only in memory while this page is open. Do not paste secrets on shared machines.
For public reads (GET /api/inventory, GET /api/shop/config), include the merchant wallet (0x-prefixed 40-hex). Non-GET requests should use JWT and will ignore this header.
Request Bodyapplication/jsonUsing server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.cURLcurl -X POST "https://api.pay.ledger1.ai/portalpay/api/graphql" -H "Content-Type: application/json" -d '{ "query": "query($wallet: ID!, $viewer: ID) { user(wallet: $wallet) { wallet displayName pfpUrl xp live liveSince lastHeartbeat spaceUrl spacePublic followersCount followingCount } follows(wallet: $wallet, viewer: $viewer) { followersCount followingCount viewerFollows } liveUsers { wallet displayName pfpUrl spaceUrl liveSince lastHeartbeat } leaderboard(limit: 10) { wallet displayName pfpUrl xp } }", "variables": { "wallet": "0x1234567890abcdef1234567890abcdef12345678", "viewer": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" } }'Response Status—Response Headers—Response Body—
Example Responses#
Success (200 OK):
json{ "data": { "user": { "wallet": "0x1234567890abcdef1234567890abcdef12345678", "displayName": "Alice", "pfpUrl": "https://cdn.example/pfp/alice.png", "xp": 1234, "live": true, "liveSince": 1698789000000, "lastHeartbeat": 1698790100000, "spaceUrl": "https://portalpay.com/alice", "spacePublic": true, "followersCount": 42, "followingCount": 7 }, "follows": { "followersCount": 42, "followingCount": 7, "viewerFollows": true }, "liveUsers": [ { "wallet": "0xaaaa...bbbb", "displayName": "Bob", "pfpUrl": "https://cdn.example/pfp/bob.png", "spaceUrl": "https://portalpay.com/bob", "liveSince": 1698789500000, "lastHeartbeat": 1698790123456 } ], "leaderboard": [ { "wallet": "0xaaaa...bbbb", "displayName": "Bob", "pfpUrl": "...", "xp": 9001 } ] } }
Error (example GraphQL error shape):
json{ "errors": [ { "message": "Bad Request", "path": ["user"] } ] }
Possible responses: 200 (OK), 429 (rate limited)
Rate Limiting and Origin Enforcement#
- APIM custom domain is primary; if AFD is enabled it injects markupwhich APIM validates.
x-edge-secret - Responses may include markup,
X-RateLimit-Limitmarkup,X-RateLimit-Remainingmarkup.X-RateLimit-Reset - On markup, implement exponential backoff.
429 Too Many Requests
Scopes#
Typical APIM scopes for GraphQL read queries:
- markup
users:read
403 ForbiddenMutations#
upsertUsersetPresencefollowcb_auth_token