> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payai.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Facilitator Authentication

> How to authenticate with the PayAI facilitator using your merchant API key — protocol reference and standalone examples in TypeScript, Python, Go, and Rust.

## Overview

The [PayAI facilitator](https://facilitator.payai.network) authenticates merchants using short-lived **JWTs signed with Ed25519** (the EdDSA algorithm). If you use the `@payai/facilitator` TypeScript package, this is handled automatically. This guide explains the underlying protocol so you can implement authentication in **any language** without depending on PayAI packages.

<Note>
  Authentication is optional for ordinary exact payments and Solana batch settlement on the public shared lane. Use an API key for paid usage, account attribution, or an assigned enterprise lane. If you send a credential, it must be valid; PayAI does not silently downgrade invalid credentials to anonymous access. Autonomous agents can [buy credits and receive a key over x402](/x402/facilitators/agent-api-keys) without portal signup; PayAI creates a wallet-owned agent account programmatically. The [merchant dashboard](https://merchant.payai.network) is the optional human-managed route.
</Note>

<Tip>
  If you're following one of the TypeScript server guides ([Express](/x402/servers/typescript/express), [Hono](/x402/servers/typescript/hono), [Next.js](/x402/servers/typescript/nextjs)), facilitator authentication is already built in. Just set the `PAYAI_API_KEY_ID` and `PAYAI_API_KEY_SECRET` environment variables and the middleware handles the rest. Refer to those guides for setup instructions.
</Tip>

## API key structure

Your API key has two parts, returned by the [portal-free agent vending flow](/x402/facilitators/agent-api-keys) or available from the [merchant dashboard](https://merchant.payai.network):

| Part | Environment Variable | Description |
| - | - | - |
| **API Key ID** | `PAYAI_API_KEY_ID` | A string identifier for your key |
| **API Key Secret** | `PAYAI_API_KEY_SECRET` | An Ed25519 private key in PKCS#8/DER format, base64-encoded |

The secret may be prefixed with `payai_sk_` as shown in the dashboard. Strip this prefix before use — the remaining string is a standard base64-encoded PKCS#8 DER key.

***

## Protocol steps

### 1. Normalize the API key secret

If the secret starts with `payai_sk_`, remove that prefix. The result is a base64-encoded Ed25519 private key in PKCS#8/DER format.

### 2. Build the JWT header

```json theme={null}
{
  "alg": "EdDSA",
  "typ": "JWT",
  "kid": "<your_api_key_id>"
}
```

### 3. Build the JWT payload

```json theme={null}
{
  "sub": "<your_api_key_id>",
  "iss": "payai-merchant",
  "iat": 1709700000,
  "exp": 1709700120,
  "jti": "550e8400-e29b-41d4-a716-446655440000"
}
```

| Claim | Description |
| - | - |
| `sub` | Your API Key ID |
| `iss` | Always `"payai-merchant"` |
| `iat` | Current time as a Unix timestamp (seconds) |
| `exp` | Expiration — `iat + 120` (2 minutes recommended) |
| `jti` | A random UUID v4 for replay protection |

### 4. Encode and sign

1. Base64url-encode the header JSON -> `headerB64`
2. Base64url-encode the payload JSON -> `payloadB64`
3. Form the signing input: `headerB64 + "." + payloadB64`
4. Sign the UTF-8 bytes of the signing input with your Ed25519 private key
5. Base64url-encode the 64-byte signature -> `signatureB64`
6. The JWT is: `headerB64.payloadB64.signatureB64`

**Base64url encoding** uses the standard Base64 alphabet with `+` replaced by `-`, `/` replaced by `_`, and no `=` padding.

### 5. Send the request

Include the JWT as a Bearer token on all facilitator requests:

```
Authorization: Bearer <jwt>
```

Send this header on `POST /verify` and `POST /settle` when using authenticated service. `GET /supported` is public. Without a token it returns the public shared-lane fee payer; a valid enterprise token can select the account's assigned fee payer.

***

## Token caching

JWTs are valid for the full `exp - iat` window (default: 120 seconds). To avoid signing on every request, cache the token and refresh it \~30 seconds before expiry.

***

## Code examples

The following examples implement the full authentication flow with **no PayAI-specific dependencies**.

<Tabs>
  <Tab title="TypeScript">
    Uses the Web Crypto API — works in Node.js 18+, Deno, Bun, and browsers with zero dependencies.

    ```typescript theme={null}
    // payai-auth.ts — zero dependencies, Web Crypto API only

    function base64UrlEncode(data: Uint8Array): string {
      let binary = "";
      for (let i = 0; i < data.byteLength; i++) {
        binary += String.fromCharCode(data[i]);
      }
      return btoa(binary)
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=+$/, "");
    }

    function base64UrlEncodeString(str: string): string {
      return base64UrlEncode(new TextEncoder().encode(str));
    }

    function base64ToUint8Array(base64: string): Uint8Array {
      const standardized = base64.replace(/-/g, "+").replace(/_/g, "/");
      const binary = atob(standardized);
      const bytes = new Uint8Array(binary.length);
      for (let i = 0; i < binary.length; i++) {
        bytes[i] = binary.charCodeAt(i);
      }
      return bytes;
    }

    function normalizeSecret(secret: string): string {
      const trimmed = secret.trim();
      return trimmed.startsWith("payai_sk_")
        ? trimmed.slice("payai_sk_".length)
        : trimmed;
    }

    export async function generatePayAIJwt(
      apiKeyId: string,
      apiKeySecret: string,
    ): Promise<string> {
      const now = Math.floor(Date.now() / 1000);

      const header = JSON.stringify({
        alg: "EdDSA",
        typ: "JWT",
        kid: apiKeyId,
      });
      const payload = JSON.stringify({
        sub: apiKeyId,
        iss: "payai-merchant",
        iat: now,
        exp: now + 120,
        jti: crypto.randomUUID(),
      });

      const headerB64 = base64UrlEncodeString(header);
      const payloadB64 = base64UrlEncodeString(payload);
      const message = `${headerB64}.${payloadB64}`;

      const keyBytes = base64ToUint8Array(normalizeSecret(apiKeySecret));
      const privateKey = await crypto.subtle.importKey(
        "pkcs8",
        keyBytes.buffer,
        { name: "Ed25519" },
        false,
        ["sign"],
      );

      const signature = await crypto.subtle.sign(
        "Ed25519",
        privateKey,
        new TextEncoder().encode(message),
      );

      return `${message}.${base64UrlEncode(new Uint8Array(signature))}`;
    }

    // --- Usage ---

    const jwt = await generatePayAIJwt(
      "your-api-key-id",
      "payai_sk_your-api-key-secret",
    );

    const response = await fetch(
      "https://facilitator.payai.network/verify",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${jwt}`,
        },
        body: JSON.stringify({
          x402Version: 2,
          paymentPayload: {
            /* ... */
          },
          paymentRequirements: {
            /* ... */
          },
        }),
      },
    );
    ```
  </Tab>

  <Tab title="Python">
    Requires the [`cryptography`](https://pypi.org/project/cryptography/) package:

    ```bash theme={null}
    pip install cryptography
    ```

    ```python theme={null}
    # payai_auth.py
    import base64
    import json
    import time
    import uuid

    from cryptography.hazmat.primitives.serialization import load_der_private_key


    def base64url_encode(data: bytes) -> str:
        return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")


    def normalize_secret(secret: str) -> str:
        s = secret.strip()
        return s.removeprefix("payai_sk_")


    def generate_payai_jwt(api_key_id: str, api_key_secret: str) -> str:
        now = int(time.time())

        header = json.dumps(
            {"alg": "EdDSA", "typ": "JWT", "kid": api_key_id},
            separators=(",", ":"),
        )
        payload = json.dumps(
            {
                "sub": api_key_id,
                "iss": "payai-merchant",
                "iat": now,
                "exp": now + 120,
                "jti": str(uuid.uuid4()),
            },
            separators=(",", ":"),
        )

        header_b64 = base64url_encode(header.encode())
        payload_b64 = base64url_encode(payload.encode())
        message = f"{header_b64}.{payload_b64}"

        key_bytes = base64.b64decode(normalize_secret(api_key_secret))
        private_key = load_der_private_key(key_bytes, password=None)
        signature = private_key.sign(message.encode())

        return f"{message}.{base64url_encode(signature)}"


    # --- Usage ---

    if __name__ == "__main__":
        import requests

        jwt = generate_payai_jwt(
            "your-api-key-id",
            "payai_sk_your-api-key-secret",
        )

        response = requests.post(
            "https://facilitator.payai.network/verify",
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {jwt}",
            },
            json={
                "x402Version": 2,
                "paymentPayload": {},   # ...
                "paymentRequirements": {},  # ...
            },
        )
        print(response.json())
    ```
  </Tab>

  <Tab title="Go">
    Uses the Go standard library plus `github.com/google/uuid` for UUID generation.

    ```go theme={null}
    package payaiauth

    import (
    	"crypto/ed25519"
    	"crypto/x509"
    	"encoding/base64"
    	"encoding/json"
    	"fmt"
    	"strings"
    	"time"

    	"github.com/google/uuid"
    )

    func base64URLEncode(data []byte) string {
    	return strings.TrimRight(
    		base64.URLEncoding.EncodeToString(data), "=",
    	)
    }

    func normalizeSecret(secret string) string {
    	s := strings.TrimSpace(secret)
    	return strings.TrimPrefix(s, "payai_sk_")
    }

    func GeneratePayAIJwt(apiKeyID, apiKeySecret string) (string, error) {
    	now := time.Now().Unix()

    	header, _ := json.Marshal(map[string]string{
    		"alg": "EdDSA",
    		"typ": "JWT",
    		"kid": apiKeyID,
    	})
    	payload, _ := json.Marshal(map[string]interface{}{
    		"sub": apiKeyID,
    		"iss": "payai-merchant",
    		"iat": now,
    		"exp": now + 120,
    		"jti": uuid.New().String(),
    	})

    	headerB64 := base64URLEncode(header)
    	payloadB64 := base64URLEncode(payload)
    	message := headerB64 + "." + payloadB64

    	keyBytes, err := base64.StdEncoding.DecodeString(
    		normalizeSecret(apiKeySecret),
    	)
    	if err != nil {
    		keyBytes, err = base64.URLEncoding.DecodeString(
    			normalizeSecret(apiKeySecret),
    		)
    		if err != nil {
    			return "", fmt.Errorf("decode API key secret: %w", err)
    		}
    	}

    	parsed, err := x509.ParsePKCS8PrivateKey(keyBytes)
    	if err != nil {
    		return "", fmt.Errorf("parse PKCS#8 key: %w", err)
    	}

    	privateKey, ok := parsed.(ed25519.PrivateKey)
    	if !ok {
    		return "", fmt.Errorf("key is not Ed25519")
    	}

    	signature := ed25519.Sign(privateKey, []byte(message))
    	return message + "." + base64URLEncode(signature), nil
    }
    ```

    Usage:

    ```go theme={null}
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"net/http"

    	payaiauth "your-module/payaiauth"
    )

    func main() {
    	jwt, err := payaiauth.GeneratePayAIJwt(
    		"your-api-key-id",
    		"payai_sk_your-api-key-secret",
    	)
    	if err != nil {
    		panic(err)
    	}

    	body, _ := json.Marshal(map[string]interface{}{
    		"x402Version":         2,
    		"paymentPayload":      map[string]interface{}{},
    		"paymentRequirements": map[string]interface{}{},
    	})

    	req, _ := http.NewRequest(
    		"POST",
    		"https://facilitator.payai.network/verify",
    		bytes.NewReader(body),
    	)
    	req.Header.Set("Content-Type", "application/json")
    	req.Header.Set("Authorization", "Bearer "+jwt)

    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println("Status:", resp.Status)
    }
    ```

    <Note>
      You can replace `github.com/google/uuid` with `crypto/rand` to generate a UUID v4 manually and avoid the external dependency entirely.
    </Note>
  </Tab>

  <Tab title="Rust">
    Add to `Cargo.toml`:

    ```toml theme={null}
    [dependencies]
    ed25519-dalek = { version = "2", features = ["pkcs8"] }
    base64 = "0.22"
    serde_json = "1"
    uuid = { version = "1", features = ["v4"] }
    ```

    ```rust theme={null}
    use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
    use base64::Engine;
    use ed25519_dalek::pkcs8::DecodePrivateKey;
    use ed25519_dalek::{Signer, SigningKey};
    use serde_json::json;
    use std::time::{SystemTime, UNIX_EPOCH};
    use uuid::Uuid;

    fn normalize_secret(secret: &str) -> &str {
        let s = secret.trim();
        s.strip_prefix("payai_sk_").unwrap_or(s)
    }

    pub fn generate_payai_jwt(
        api_key_id: &str,
        api_key_secret: &str,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)?
            .as_secs();

        let header = json!({
            "alg": "EdDSA",
            "typ": "JWT",
            "kid": api_key_id,
        });
        let payload = json!({
            "sub": api_key_id,
            "iss": "payai-merchant",
            "iat": now,
            "exp": now + 120,
            "jti": Uuid::new_v4().to_string(),
        });

        let header_b64 = URL_SAFE_NO_PAD.encode(
            header.to_string().as_bytes(),
        );
        let payload_b64 = URL_SAFE_NO_PAD.encode(
            payload.to_string().as_bytes(),
        );
        let message = format!("{header_b64}.{payload_b64}");

        let key_bytes = STANDARD.decode(
            normalize_secret(api_key_secret),
        )?;
        let signing_key = SigningKey::from_pkcs8_der(&key_bytes)?;
        let signature = signing_key.sign(message.as_bytes());

        Ok(format!(
            "{message}.{}",
            URL_SAFE_NO_PAD.encode(signature.to_bytes()),
        ))
    }

    // Usage:
    // let jwt = generate_payai_jwt("your-api-key-id", "payai_sk_...")?;
    // Set header: Authorization: Bearer {jwt}
    ```
  </Tab>
</Tabs>

***

## Facilitator endpoints

All endpoints are at `https://facilitator.payai.network`. Note that `/verify` validates the request body before the token, so a malformed request returns `400` rather than `401` even when the token is missing or bad.

| Endpoint | Method | Auth | Description |
| - | - | - | - |
| `/verify` | POST | optional for the public shared lane; required for assigned enterprise lanes and authenticated usage | Verify a payment payload against payment requirements |
| `/settle` | POST | required for batch and beyond the ordinary exact free tier | Settle a verified payment on-chain |
| `/supported` | GET | not required | List supported networks, tokens, and schemes |

### Request body (`/verify` and `/settle`)

```json theme={null}
{
  "x402Version": 2,
  "paymentPayload": {},
  "paymentRequirements": {}
}
```

This is a structural outline, not a valid payment request. Fill both objects using
the complete examples in the [x402 Reference](/x402/reference).

***

## Troubleshooting

| Error | Cause | Fix |
| - | - | - |
| `401 Unauthorized` | Missing or expired JWT | Generate a fresh token (check `exp` claim) |
| Failed to parse key | Secret not valid PKCS#8/DER | Ensure you stripped the `payai_sk_` prefix and the remaining string is valid base64 |
| Signature verification failed | Wrong key or corrupted secret | Verify your API Key ID and Secret match the [merchant dashboard](https://merchant.payai.network) |

## Need help?

<Card title="Join our Community" icon="discord" href="https://discord.gg/eWJRwMpebQ">
  Have questions or want to connect with other developers? Join our Discord server.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.