An investment in knowledge pays the best interest.
Benjamin Franklin

Graphql

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
    markup
    x-edge-secret
    header per policy.
  • Rate limit headers (if enabled):
    markup
    X-RateLimit-Limit
    ,
    markup
    X-RateLimit-Remaining
    ,
    markup
    X-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)

Headers:

markup
Ocp-Apim-Subscription-Key: {your-subscription-key}
Content-Type: application/json

Notes:

  • Only read queries are available via APIM developer subscriptions.
  • Mutations (e.g.,
    markup
    upsertUser
    ,
    markup
    setPresence
    ,
    markup
    follow
    ) are not available via APIM developer subscriptions and must be performed within the PortalPay Admin UI where JWT cookies and CSRF protections apply.
  • 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
    user(wallet: ID!): User
    — Fetch a user profile by wallet address
  • markup
    follows(wallet: ID!, viewer: ID): FollowsInfo!
    — Followers/following counts and whether
    markup
    viewer
    follows
    markup
    wallet
  • markup
    liveUsers: [LiveUser!]!
    — Users considered currently live and public
  • markup
    leaderboard(limit: Int = 50): [User!]!
    — Top users ranked by XP (max 200)

Important return types:

  • markup
    User
    fields include:
    markup
    wallet
    ,
    markup
    displayName
    ,
    markup
    bio
    ,
    markup
    pfpUrl
    ,
    markup
    xp
    ,
    markup
    followersCount
    ,
    markup
    followingCount
    ,
    markup
    live
    ,
    markup
    liveSince
    ,
    markup
    lastHeartbeat
    ,
    markup
    spaceUrl
    ,
    markup
    spacePublic
  • markup
    LiveUser
    fields include:
    markup
    wallet
    ,
    markup
    displayName
    ,
    markup
    pfpUrl
    ,
    markup
    spaceUrl
    ,
    markup
    liveSince
    ,
    markup
    lastHeartbeat
  • markup
    FollowsInfo
    fields:
    markup
    followersCount
    ,
    markup
    followingCount
    ,
    markup
    viewerFollows

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/graphql

Try 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 Body
application/json
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.
cURL
curl -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
    markup
    x-edge-secret
    which APIM validates.
  • Responses may include
    markup
    X-RateLimit-Limit
    ,
    markup
    X-RateLimit-Remaining
    ,
    markup
    X-RateLimit-Reset
    .
  • On
    markup
    429 Too Many Requests
    , implement exponential backoff.

Scopes#

Typical APIM scopes for GraphQL read queries:

  • markup
    users:read
Requests missing the required scope return
markup
403 Forbidden
.

Mutations#

Mutations (
markup
upsertUser
,
markup
setPresence
,
markup
follow
) are not available via APIM developer subscriptions. Perform administrative actions within the PortalPay Admin UI, where JWT cookies (
markup
cb_auth_token
), CSRF protections, and role checks are enforced.