Building on YieldFabric — single-read entry for LLMs

Wire-level reference for LLMs (and humans) building apps on YieldFabric. Auth flows, the five most-common operations, error handling, common pitfalls. Pragmatic companion to the alignment-economy thesis.

Guides/Start here

You are an LLM tasked with building an app on top of YieldFabric. This file is the wire-level reference — auth flows, endpoints, mutation shapes, error handling, common pitfalls. Pragmatic and HTTP-shaped. Read it once; then build.

Read the alignment-economy guide first if you haven't. It's the lens for why the platform is shaped the way it is — the five operational roles (reason → communicate → negotiate → structure → execute), what role AI plays in each, and why the Intent → Confirm boundary is the most important architectural element in any application you build on YF. This file tells you how to call the API; that guide tells you what you're calling it for.

What YieldFabric is

YieldFabric is infrastructure for the alignment economy — a substrate where humans align on new agreements through AI-augmented conversation, with the moment of agreement performed by the humans (via the Intent → Confirm boundary) and AI doing translation, knowledge access, and proposal composition on the edges.

Concretely: confidential on-chain payments and obligations, multi-agent orchestration, working-group collaboration. AI proposes, humans confirm, the system executes. The financial primitives (payments, swaps, repos) and the agentic primitives (workspaces, threads, knowledge graphs) are not two separate stacks — they are the same alignment process at different operational stages.

A YieldFabric app talks to four public hosts:

Surface
Identity
URL
auth.yieldfabric.com
What it's for
Sign-in (email, signature, OAuth providers), JWT issuance + refresh, API-key issuance, account + key management. REST.
Surface
Payments direct
URL
pay.test.yieldfabric.com (chain 153) / pay.live.yieldfabric.com (chain 151)
What it's for
On-chain GraphQL mutations, pay/MQ-backed or chain-RPC GraphQL reads, instance-local activity/status REST, SSE and WebSocket. Select from the signed JWT's default_chain_id.
Surface
GraphQL gateway
URL
api.yieldfabric.com/graphql
What it's for
Federated and cross-subgraph reads plus auth/agents mutations. The payments subgraph is statically pinned to testnet for shared-core reads.
Surface
Agents
URL
agents.yieldfabric.com
What it's for
Working groups, threads, conversation streams, and the agent runtime that helps your users structure deals. REST + Server-Sent Events, when your app uses the AI collaboration surface.

The rule of thumb: gateway for federated/shared-core reads, JWT-selected payments-direct GraphQL for chain-bound mutations and instance-local reads, auth REST for sign-in, the same selected payments host for settlement/events, and agents REST/SSE for conversation state. That's the whole map.

One alignment process at five operational stages. The financial primitives and the agentic primitives are not parallel stacks — they are the same alignment process at different stages (see the alignment-economy guide). Reason (knowledge graphs, agentic pipelines) and communicate (working groups, threads, agents-as-edge-translators) and negotiate (intents, drafts, Studio) live primarily in the agents service; structure (contracts, swaps, repos) and execute (payments, settlement) live primarily in the payments service. The Intent → Confirm boundary is the structural locus where wanting collapses into a definite joint prompt — the most important UI element in any application you build.

Before wiring the financial-side endpoints below, read /docs/guides/agents-and-workspaces for the reason/communicate/negotiate side. That's where most applications spend most of their time.

The URLs your app calls — and the ones it shouldn't

A consumer app uses these public surfaces, and nothing else:

Public surface
POST https://auth.yieldfabric.com/auth/** (REST)
For
Sign-in (/auth/login, /auth/signature/signin, /auth/<provider>/exchange), token refresh, GET /protected/jwt to resolve the caller, and invitations (/auth/invitations/** — create / list / accept; GET /auth/invitations/{token} is public for the magic-link landing).
Public surface
POST https://api.yieldfabric.com/graphql (GraphQL gateway)
For
Federated/cross-subgraph reads and auth/agents mutations.
Public surface
POST <selected-payments-host>/graphql
For
Chain-bound payment mutations plus mode-bound reads (swap intents, ZK documents, policy/approval and oracle-condition status). Select pay.test for JWT chain 153 and pay.live for chain 151.
Public surface
GET <selected-payments-host>/api/users/{user_id}/messages/{message_id}
For
Poll the message returned by an on-chain mutation until executed is non-null. Use the same host as the mutation.
Public surface
https://agents.yieldfabric.com/** (agents REST/SSE)
For
Working groups, threads, documents, pipelines, workflow streams, and agent conversation state.

Do not route payments work through raw service-to-service endpoints. Use the corresponding payments-direct GraphQL mutation, then poll the message-status endpoint on that same instance.

Internal surface — do not use from app code
<payments-host>/api/mq/submit (raw queue submission)
Why
Service-to-service write path. Bypasses GraphQL resolvers' input normalisation, idempotency, and entity resolution. Also requires the caller to be a YF service with a registered vault key pair (you'll get 403 Forbidden — Insufficient permissions: CryptoOperations otherwise). Use payments-direct GraphQL mutations instead.
Internal surface — do not use from app code
<payments-host>/api/chain/** (chain reads)
Why
Service-to-service helper for resolving on-chain constants. The GraphQL resolvers already do this for you.
Internal surface — do not use from app code
agents.yieldfabric.com/api/mcp/** (MCP transport)
Why
LLM tool-call surface, not data flow.

Authenticate in 30 seconds

Four sign-in paths. Browser clients use email/password, wallet signature, or provider exchange (#1, #2, #4). Backend services (app backends, scheduled workers, integration scripts) use API keys (#3).

1. Email + password (easiest)

LOGIN=$(curl -s -X POST https://auth.yieldfabric.com/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"…"}')

TOKEN=$(echo "$LOGIN" | jq -r .token)
REFRESH=$(echo "$LOGIN" | jq -r .refresh_token)
USER_ID=$(echo "$LOGIN" | jq -r .user.id)

Access tokens last 15 min. Refresh tokens last 30 days, single-use, rotated on every refresh.

2. Wallet signature (for wallet-led users)

# 1. Nonce
NONCE=$(curl -s https://auth.yieldfabric.com/auth/signature/nonce | jq -r .nonce)

# 2. Wallet signs $NONCE off-chain (EIP-191 personal_sign)

# 3. Exchange
curl -X POST https://auth.yieldfabric.com/auth/signature/signin \
  -H 'Content-Type: application/json' \
  -d "{\"public_key\":\"0x04…\",\"signature\":\"0x…\",\"message\":\"$NONCE\",\"nonce\":\"$NONCE\"}"

3. API key (for backend services — what your server-side code should use)

For non-browser callers — your app's backend, scheduled workers, batch jobs, anything that doesn't log a human in at boot — don't store an email + password in env vars. Instead:

  1. Authenticate once, manually, as the service-account user with any of the user-facing flows (email/password is fine here — it's one-time).
  2. Mint a long-lived API key for that user:
    curl -X POST https://auth.yieldfabric.com/auth/api-key/generate \
      -H "Authorization: Bearer $ONE_TIME_TOKEN" \
      -H 'Content-Type: application/json' \
      -d '{"service_name":"my-app-backend","description":"Application backend"}'
    # → { "api_key": "yf_api_…", "service_name": "…", … }
    
    The raw yf_api_… value is returned only at creation time; the server stores its hash only. Save it; store it as API_KEY in your service's env.
  3. At runtime, exchange the API key for a short-lived JWT on every boot and on every refresh:
    curl -X POST https://auth.yieldfabric.com/auth/api-key \
      -H 'Content-Type: application/json' \
      -d '{"api_key":"yf_api_…"}'
    # → same AuthResponse shape as /auth/login
    

Why this matters: API keys can be revoked individually (POST /auth/api-keys/{key_id}/revoke) and audited (GET /auth/api-keys) without rotating the underlying user's password. They're also the canonical answer to "how does my server-side code authenticate" — this is the recommended pattern for any non-browser service that needs to call YF.

Full reference: the API-keys section of the auth reference.

4. Provider exchange (Averer / MetaMask / Email / WebAuthn)

# Discover available providers in this deployment
curl https://auth.yieldfabric.com/auth/providers/config

# Per-provider exchange — see docs at /docs/api/auth (on the running website)
curl -X POST https://auth.yieldfabric.com/auth/metamask/exchange \
  -H 'Content-Type: application/json' \
  -d '{"address":"0x…","signature":"0x…","message":"<contains nonce>","chain_id":1}'

All four token exchange paths return the same LoginResponse shape:

{
  "token": "<access JWT, 15min>",
  "refresh_token": "<refresh, 30d, single-use>",
  "token_type": "Bearer",
  "expires_in": 900,
  "user": { "id": "…", "email": "…", "role": "…" }
}

Live/Test mode is a JWT routing claim

Production login starts on chain 153 (Test). The browser reads the signed JWT's default_chain_id only to select a configured payments URL; the chosen payments instance still validates the token and rejects writes whose claim does not match its pinned chain.

Switching mode is an explicit two-step ceremony:

  1. POST /entities/{user|group}/{id}/default-chain {"chain_id":"151"} activates or deploys the target-chain account.
  2. POST /auth/refresh {"refresh_token":"…","chain_id":"151"} rotates the session so every direct payment transport reconnects to mainnet.

The first-party app configures both hosts but separately declares which chains are enabled. Until mainnet contracts are deployed, only 153 is enabled and Live is visibly unavailable. Enabling chain 151 and the payments-dual deployment profile unlocks Live without a code change. When acting as a group, the ceremony activates both the personal and group wallets and then re-mints the delegation JWT on the same target chain.

Make your first authenticated request

Use the JWT as Authorization: Bearer <token> on any public surface. Same JWT works for auth REST, the GraphQL gateway, payments status polling, and agents REST.

# Auth: get current user
curl https://auth.yieldfabric.com/auth/users/me -H "Authorization: Bearer $TOKEN"

# Gateway GraphQL: list payments visible to your entity
curl -X POST https://api.yieldfabric.com/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"query":"query { paymentsByEntity { id status amount assetId } }"}'

# Agents: list working groups
curl https://agents.yieldfabric.com/working-groups -H "Authorization: Bearer $TOKEN"

The seven things you'll do most

1. Refresh the token

Access tokens expire in 15 min. Rotate before they do:

# Resolve the active mode from the authenticated session. Browser clients read
# the same signed `default_chain_id` claim as a routing hint.
CHAIN_ID=$(curl -s https://auth.yieldfabric.com/protected/jwt \
  -H "Authorization: Bearer $TOKEN" | jq -r .default_chain_id)
case "$CHAIN_ID" in
  153) PAYMENTS_BASE=https://pay.test.yieldfabric.com ;;
  151) PAYMENTS_BASE=https://pay.live.yieldfabric.com ;;
  *) echo "Unsupported payment chain: $CHAIN_ID" >&2; exit 1 ;;
esac
PAYMENTS_GRAPHQL="$PAYMENTS_BASE/graphql"

NEW=$(curl -s -X POST https://auth.yieldfabric.com/auth/refresh \
  -H 'Content-Type: application/json' \
  -d "{\"refresh_token\":\"$REFRESH\",\"chain_id\":\"$CHAIN_ID\"}")
TOKEN=$(echo "$NEW" | jq -r .access_token)
REFRESH=$(echo "$NEW" | jq -r .refresh_token)

CHAIN_ID is the current JWT's default_chain_id; sending a different served chain is the second half of the explicit mode-switch ceremony, after that entity's target-chain wallet has been activated. Refresh tokens are single-use. If you get a 401 here, the user must re-sign-in.

2. Send a payment

curl -X POST "$PAYMENTS_GRAPHQL" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "mutation($input: InstantSendInput!) { instant(input: $input) { success messageId paymentId } }",
    "variables": { "input": {
      "destinationWalletId": "<recipient wallet UUID>",
      "assetId": "aud-token-asset",
      "amount": "10",
      "idempotencyKey": "<fresh UUIDv4>"
    }}
  }'

destinationWalletId is the canonical recipient identifier when you have a wallet UUID. If you only have a name or email, use destinationId as an entity-name lookup. Do not pass a raw 0x… address to destinationId; resolve the wallet first and pass destinationWalletId. The full input shape (incl. destinationWalletId, contractId, requireManualSignature, …) lives in the GraphQL schema (browseable in /docs/api/payments) under input InstantSendInput.

Idempotency key rule — omit the field and let the resolver generate one, or pass a fresh UUIDv4 for each submission. Same key + same body = same response (no double-charge); same key + different body = 400 IDEMPOTENCY_CONFLICT. Never use a deterministic key.

The recipient must accept incoming payments. A confidential payment lands as pending in the recipient's wallet — it isn't spendable until accepted. Accept on demand with acceptAll, or arm a wallet so a background worker accepts its incoming automatically (one asset, a set, or all). Full developer guide: Auto-accept incoming payments.

3. Mint and accept an obligation (zero-payment agreement flow)

An Obligation is a counterparty agreement that can carry payments or be a pure agreement (T&C, attestation, role acceptance). Mint from the admin / issuer side, accept from the counterparty side. Both go through the JWT-selected payments-direct GraphQL endpoint.

# 1. Mint — admin's Bearer. The resolver derives the minter's account
#    from the JWT, picks the obligation contract address from the
#    chain registry, generates the on-chain token id, AND auto-generates
#    a fresh idempotency_key. You only supply the counterpart and the
#    off-chain `data` blob.
CREATE_RESP=$(curl -s -X POST "$PAYMENTS_GRAPHQL" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "mutation($input: CreateObligationInput!) { createObligation(input: $input) { success contractId obligationResult messageId message } }",
    "variables": { "input": {
      "counterpartWalletId": "<UUID from /protected/jwt default_wallet_id>",
      "data": { "kind": "tnc", "version": "v1.0", "title": "...", "body_sha256": "..." }
    }}
  }')
# → { "createObligation": { "success": true, "contractId": "CONTRACT-OBLIGATION-...",
#                            "obligationResult": null, "messageId": "<uuid>", ... } }

# 2. Wait for the on-chain mint to settle. The mutation returns as
#    soon as the message is enqueued; the contract isn't readable by
#    acceptObligation until the executor finishes. Poll every 2s
#    until `executed` is set (matches the YF Python SDK's
#    poll_message_completion).
MSG_ID=$(echo "$CREATE_RESP" | jq -r .data.createObligation.messageId)
ADMIN_USER_ID=$(curl -s https://auth.yieldfabric.com/protected/jwt -H "Authorization: Bearer $ADMIN_TOKEN" | jq -r .user_id)
until curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
  "$PAYMENTS_BASE/api/users/$ADMIN_USER_ID/messages/$MSG_ID" \
  | jq -e '.executed' > /dev/null; do sleep 2; done

# 3. Accept — counterpart's Bearer (the user). Keys off `contractId`
#    from the create response.
CONTRACT_ID=$(echo "$CREATE_RESP" | jq -r .data.createObligation.contractId)
ACCEPT_RESP=$(curl -s -X POST "$PAYMENTS_GRAPHQL" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{
    \"query\": \"mutation(\$input: AcceptObligationInput!) { acceptObligation(input: \$input) { success message obligationId messageId } }\",
    \"variables\": { \"input\": { \"contractId\": \"$CONTRACT_ID\" } }
  }")

# 4. Wait for the accept message to settle the same way, this time
#    polling under the user's identity since the message belongs to
#    their entity.
ACCEPT_MSG_ID=$(echo "$ACCEPT_RESP" | jq -r .data.acceptObligation.messageId)
USER_USER_ID=$(curl -s https://auth.yieldfabric.com/protected/jwt -H "Authorization: Bearer $USER_TOKEN" | jq -r .user_id)
until curl -s -H "Authorization: Bearer $USER_TOKEN" \
  "$PAYMENTS_BASE/api/users/$USER_USER_ID/messages/$ACCEPT_MSG_ID" \
  | jq -e '.executed' > /dev/null; do sleep 2; done

Things that trip people up — all of these are pitfalls real apps have hit during their first integration:

  • counterpart is an entity name, not an address. Passing 0x… fails with "No entity found with name '0x…'". Use counterpartWalletId whenever you have the wallet UUID (every authenticated caller does — /protected/jwt returns default_wallet_id). Same caveat applies to obligor / obligorWalletId.
  • Don't pass a deterministic idempotencyKey from app code (e.g. sha256(user || version)). The resolver dedupes on it; a retry after a stuck message just returns the stuck messageId again without re-enqueueing. Either omit it (the resolver auto-generates a fresh one from timestamp+user) or use a UUIDv4 per submission.
  • contractIdobligationResult ≠ on-chain id. contractId is the YF-side identifier; that's what acceptObligation keys off. The on-chain BigUint obligation id comes back as obligationResult on create (often null pre-settle) and as obligationId on accept. Save both — the on-chain id is your durable receipt, the contractId is the YF handle.
  • Zero-payment obligations omit denomination, obligor/obligorWalletId, and initialPayments. The resolver treats this as a pure agreement.
  • The caller minting the obligation needs CryptoOperations permission, since the executor's vault retrieves the minter's default key pair from /keys/users/{id}/default-key, which gates on that permission. For users created with role: SuperAdmin / Admin / Manager, the JWT mint falls back to role-default permissions when user_permissions is empty; for narrower roles (Viewer, ApiClient, …) you need to grant CryptoOperations explicitly via POST /auth/users/{id}/permissions.
  • Make an obligation NFT a transferable spend authority. A data policy can name an obligation NFT (collection, tokenId) as its executor: whoever currently holds that NFT may run bounded executeUnderPolicy sends from a group account, and transferring the NFT transfers the right (the property-management "agency credential" pattern). Pass the collection in executorAccounts and the non-zero token id in executorIds to addDataPolicy; the on-chain gate reads ownerOf live at every call. See the NFT-holder policy executors guide for the lifecycle, the mandatory amount bound, and the security model.

4. List a wallet's activity (history feed)

Unified, paginated feed merging the wallet's payments and on-chain messages, newest first. Use this instead of stitching payments.byWallet and /api/users/.../messages client-side.

WALLET_ID="..."  # /protected/jwt returns default_wallet_id

curl -X POST "$PAYMENTS_GRAPHQL" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "query Activity($walletId: String!, $limit: Int, $cursor: String) {
      walletFlow {
        activity(walletId: $walletId, limit: $limit, cursor: $cursor) {
          nextCursor
          items {
            __typename
            ... on PaymentActivity { timestamp payment { id status amount assetId } }
            ... on MessageActivity { timestamp message { id messageType status executed } }
          }
        }
      }
    }",
    "variables": { "walletId": "'"$WALLET_ID"'", "limit": 25 }
  }'

Pagination contract:

  • cursor is opaque. Don't parse it. Pass nextCursor from the previous page back in as the next cursor.
  • nextCursor: null means the feed is exhausted — stop.
  • limit defaults to 25, clamped to 200.
  • The same (walletId, limit, cursor) triple is guaranteed to return the same page regardless of which replica answers (stateless, no sticky sessions).

The feed is sorted by timestamp DESC across both streams. Within a microsecond tie, items break by id lexicographically — stable but arbitrary across __typename. Don't depend on relative order of items that share a timestamp.

5. Poll for message status

Long-running on-chain operations (deposit / send / swap / create obligation) submit a message, return a messageId, then execute asynchronously. Poll:

curl "$PAYMENTS_BASE/api/users/$USER_ID/messages/$MESSAGE_ID" \
  -H "Authorization: Bearer $TOKEN"

Status field progression: PendingValidatingExecutingCompleted (or Failed / ManualSignature). See MessageStatus enum.

6. Subscribe to live events (SSE)

For real-time UI, open a Server-Sent Events stream rather than polling:

# Working-group chat stream (new messages, agent responses, typing indicators)
curl -N "https://agents.yieldfabric.com/working-groups/$GROUP_ID/chat/stream" \
  -H "Authorization: Bearer $TOKEN"

# Working-group workflow stream (step start / complete / fail)
curl -N "https://agents.yieldfabric.com/working-groups/$GROUP_ID/workflows/$WORKFLOW_ID/stream" \
  -H "Authorization: Bearer $TOKEN"

# Pipeline events (KG mutations, intent confirmation prompts)
curl -N "https://agents.yieldfabric.com/pipelines/$PIPELINE_RUN_ID/events" \
  -H "Authorization: Bearer $TOKEN"

Threads + workflows are scoped to a working group; the SSE routes follow that nesting. The full event surface is enumerated in docs/getting-started/webhooks-and-events.md. Server sends event: <type>\ndata: <json>\n\n frames. Reconnect on drop; use polling on reconnect to catch up missed state.

7. Invite someone to join (and optionally execute an agreement)

A magic-link invitation onboards a new user and routes them into a typed action on accept. target_kind is one of none | obligation | deal_flow | group_join | connection; the optional target_payload carries the terms (e.g. an obligation's amount / denomination / expiry).

# 1. Inviter creates the invitation. The email is sent automatically; the
#    response also returns the magic link to share out-of-band.
INV=$(curl -s -X POST https://auth.yieldfabric.com/auth/invitations \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "invitee_email": "them@example.com",
    "message": "Join me on YieldFabric",
    "target_kind": "connection"
  }')
INVITE_URL=$(echo "$INV" | jq -r .invite_url)        # {app}/invite/{token}
TOKEN_VALUE=$(echo "$INV" | jq -r .invitation.token)

# 2. The recipient previews the invitation BEFORE signing up — public, no auth.
curl -s "https://auth.yieldfabric.com/auth/invitations/$TOKEN_VALUE"

# 3. The recipient opens INVITE_URL, onboards (becomes a user), and obtains a
#    JWT ($INVITEE_TOKEN). Their account email must match the invited address.

# 4. Accept — returns the typed next action to route the new user into.
curl -s -X POST "https://auth.yieldfabric.com/auth/invitations/$TOKEN_VALUE/accept" \
  -H "Authorization: Bearer $INVITEE_TOKEN"
# => { "invitation": {…}, "next_action": { "kind": "connection", "payload": {…} } }

Things to know:

  • GET /auth/invitations/{token} is public — it returns a redacted view (inviter name, message, a curated target preview) and marks the invitation opened. Never send a bearer token to it.
  • Accept requires the caller's email to match invitee_email (the link is bound to that inbox) — otherwise 403. Onboard the invitee with the invited address; the app prefills + locks it in the onboarding wizard.
  • Follow next_action after accept: obligation → the accept-obligation surface, deal_flow → the deal, group_join → the group, none / connection → home / contacts. connection also forms the inviter↔invitee connection server-side.
  • Lifecycle is pending → opened → accepted (terminal revoked / expired). Resend is rate-limited; DELETE /auth/invitations/{token} is an inviter-only soft revoke.

8. Handle errors

Service
auth REST
Wire shape
Flat: { "error": "<string>" }. Route on HTTP status.
Service
GraphQL gateway
Wire shape
HTTP 200 + errors[].extensions.code in envelope. Switch on code, not message.
Service
agents REST
Wire shape
Same as auth (flat).

Status codes (consistent across services):

  • 200/201/204 — success
  • 400 — validation failed, idempotency conflict, business-rule violation
  • 401 — missing / expired / wrong-audience JWT (re-refresh)
  • 403 — JWT valid but role/scope lacks permission
  • 404 — resource doesn't exist (or visibility-scoped to a group you're not in)
  • 429 — rate-limited (login failures only; the response is delayed but still returns 401)
  • 5xx — server-side; retry with same idempotencyKey

GraphQL error codes you'll see most: INSUFFICIENT_BALANCE, IDEMPOTENCY_CONFLICT, CONTRACT_STATE_INVALID, SWAP_STATE_INVALID, DEAL_STATE_INVALID, COUNTERPART_NOT_FOUND, FORBIDDEN.

9. Check a deployment's API version before relying on a feature

Each service exposes an unauthenticated GET /version probe (a sibling of /health). Use it to confirm the deployment you're talking to actually serves the API version that documents the feature you're about to call — useful at integration time, in a readiness gate, or when a feature appears to be "missing" at runtime.

curl https://auth.yieldfabric.com/version
curl "$PAYMENTS_BASE/version"                # selected payments REST host
curl https://agents.yieldfabric.com/version
# → {"service":"yieldfabric-agents","api_version":"0.12.1","git_sha":"…","built_at":"…"}

/version is a per-service REST route — hit each service on its own host (the same hosts you use for auth REST, payments status polling, and agents REST), not the GraphQL gateway.

The response is {service, api_version, git_sha, built_at}. api_version is the service's OpenAPI info.version, baked into the binary at compile time from its own spec — so a deployment can never report a version it doesn't implement. If api_version is older than the version that introduced a feature you depend on, you're pointed at a stale deployment; that's the mismatch /version exists to surface (previously you'd have had to probe individual routes for presence). No Authorization header is needed — you can check this before holding any credential. Full reference: docs/versioning.md §"Discovering the served version".

10. Read your LLM usage and cost (agents)

If your app uses the agents LLM surface (native chat, the OpenAI-compatible /v1 endpoint, RAG, workflows), the agents service meters every call and exposes it back to you, entity-scoped — you see your own usage; staff may pass an entity_id for any entity.

# Live cross-surface rollup, grouped by (feature, model), over a window.
# Up-to-the-second; counts failed calls; spans every surface you used.
curl "https://agents.yieldfabric.com/api/usage/aggregate?start_date=2026-06-01&end_date=2026-06-16" \
  -H "Authorization: Bearer $TOKEN"
# → { "data": [ { "feature": "chat", "model": "…", "calls": N,
#                 "prompt_tokens": N, "completion_tokens": N,
#                 "total_tokens": N, "error_count": N, "avg_latency_ms": N }, … ],
#     "totals": { "calls": N, "prompt_tokens": N, "completion_tokens": N,
#                 "total_tokens": N, "error_count": N } }

# Per-call audit: the exact prompt messages + output for one metered call.
# The usage_event_id is the `id` on a GET /api/usage/detail row.
curl "https://agents.yieldfabric.com/api/usage/calls/$USAGE_EVENT_ID/log" \
  -H "Authorization: Bearer $TOKEN"

Things to know:

  • Streaming chat calls are audited too. A streamed completion (every chat answer) records both its token usage and a call-log transcript, so it shows up in /api/usage/calls/{id}/log — no more "no log for this call" on streamed responses.
  • Embeddings are logless by design. Embedding calls (feature embed) are metered — they appear in /api/usage/aggregate with their token counts — but carry no call-log transcript, so don't expect /api/usage/calls/{id}/log to return one for them.
  • avg_latency_ms and error_count come from the raw usage table, so the aggregate includes failures and is up-to-the-second (unlike the day-bucketed /api/usage/summary).
  • Both endpoints require a bearer JWT and are entity-scoped; a non-staff caller asking for another entity's entity_id gets 403, and opening another entity's call log gets 403 (or 404 if call logging is disabled / the row aged out of retention).

Service URLs at a glance

URL
https://auth.yieldfabric.com/auth/**
Use
Auth REST (login, refresh, groups, keys, identity providers)
URL
https://api.yieldfabric.com/graphql
Use
GraphQL gateway — federated/cross-subgraph reads and auth/agents mutations.
URL
https://pay.test.yieldfabric.com/graphql
Use
Chain-153 Test payment mutations and instance-local GraphQL reads.
URL
https://pay.live.yieldfabric.com/graphql
Use
Chain-151 Live payment mutations and instance-local GraphQL reads (enabled only after mainnet deployment).
URL
https://agents.yieldfabric.com/**
Use
Agents REST (working groups, threads, workflows, KGs, conversation)
URL
<selected-payments-host>/api/users/{user_id}/messages*
Use
Payments messages REST (poll status on the same instance that accepted the mutation)

Where to find detailed reference

You need
Every auth endpoint (~130) with code samples
Look at
/docs/api/auth
You need
Every payments operation (~55)
Look at
/docs/api/payments
You need
Every agents endpoint (~229)
Look at
/docs/api/agents
You need
Cross-service flow walkthrough (signup → deposit → payment → group → deal)
You need
LLM access / chat in your app (OpenAI-compatible /v1, tools, RAG grounding, reasoning, usage metering)
You need
Webhook + SSE + polling event surfaces
You need
SDK generation (TypeScript / Python / Rust / Go)
You need
MCP integration
You need
Versioning + deprecation policy

The OpenAPI specs (3 of them) are the source of truth. Browse them in the API explorer at /docs/api/<service>, or download the bundled JSON for each service to generate SDKs from.

Type-safe clients

Generate TypeScript types from the served specs:

# Once
npm i -g openapi-typescript

# Download the bundled specs
curl -o /tmp/yf-auth.json     https://yieldfabric.com/api/openapi/auth.json
curl -o /tmp/yf-payments.json https://yieldfabric.com/api/openapi/payments.json
curl -o /tmp/yf-agents.json   https://yieldfabric.com/api/openapi/agents.json

# Generate types
npx openapi-typescript /tmp/yf-auth.json     -o src/types/yf-auth.ts
npx openapi-typescript /tmp/yf-payments.json -o src/types/yf-payments.ts
npx openapi-typescript /tmp/yf-agents.json   -o src/types/yf-agents.ts

Then use openapi-fetch for typed fetch calls:

import createClient from 'openapi-fetch'
import type { paths } from './types/yf-auth'

const auth = createClient<paths>({
  baseUrl: 'https://auth.yieldfabric.com',
})

const { data, error } = await auth.POST('/auth/login', {
  body: { email, password },
})

Other languages: see sdks.md.

Common pitfalls (and how to avoid them)

Pitfall
Treating GraphQL error envelopes as HTTP errors
Avoid
GraphQL is always HTTP 200. Check errors[].extensions.code inside the body.
Pitfall
Looking up enums by message text
Avoid
Use code for routing; message may change between releases. Same for error strings in REST — those are human-readable.
Pitfall
Reusing the same idempotencyKey with different bodies
Avoid
Generate a fresh UUIDv4 per request. Same-key-different-body returns IDEMPOTENCY_CONFLICT.
Pitfall
Sending a deterministic idempotencyKey (e.g. sha256(user_id, action)) from app code
Avoid
The resolver dedupes on it. If a previous submission with the same key got stuck, every retry returns the stuck messageId without re-enqueueing — the executor never gets a fresh message to run. Either omit idempotencyKey (the resolver auto-generates one per call from timestamp+user) or use a fresh UUIDv4.
Pitfall
Trying to acceptObligation immediately after createObligation returns success
Avoid
The create mutation returns as soon as the submission is enqueued; the on-chain mint runs async. If you accept before the create's processing reaches terminal state, you get "Contract not found: CONTRACT-OBLIGATION-...". Poll GET /api/users/{user_id}/messages/{message_id} every 2s until executed is set first (or run a workflow that waits for it).
Pitfall
Calling acceptObligation after a self-counterpart payment-less createObligation (caller is also the counterparty AND no initialPayments)
Avoid
The on-chain safeMint inlines the acceptance for this shape — both ObligationCreated and ObligationAccepted fire in the same receipt, and the contract row lands at COMPLETED in one step. A follow-up acceptObligation call is now an idempotent no-op success — the resolver detects the COMPLETED status and returns a synthesized success response without enqueueing a duplicate submission. Callers can keep their existing two-call pattern; the resolver handles the idempotency.
Pitfall
Backend service authenticates fine but vault retrieval returns 403 Insufficient permissions: CryptoOperations
Avoid
The user behind the API key has no rows in user_permissions (this happens to anyone created via POST /auth/users on YF versions before the auto-grant landed). The JWT-mint path now falls back to role defaults when the table is empty, so users with SuperAdmin/Admin/Manager work out-of-the-box. For narrower roles, grant explicitly via POST /auth/users/{id}/permissions.
Pitfall
Passing a raw 0x… address as counterpart / obligor on createObligation (or as destinationId on instant)
Avoid
Those String fields are entity-name lookups (entitiesByName), not address matches. The resolver returns "No entity found with name '0x…'". Use the matching *WalletId field (counterpartWalletId, obligorWalletId, destinationWalletId) — pass the wallet UUID, e.g. from /protected/jwt's default_wallet_id.
Pitfall
Polling message status every 100ms
Avoid
Poll every 1-2s for fresh submissions; back off after 30s. Subscribe via SSE if you need real-time.
Pitfall
Sending chain-bound payment mutations or instance-local payments reads through the static gateway
Avoid
Use the payments-direct /graphql selected by the active JWT chain. Keep federated/shared-core reads and auth/agents mutations on api.yieldfabric.com/graphql.
Pitfall
Using SSE (/api/events/stream) or WebSocket (/ws/messages) to wait for message completion from a backend service
Avoid
Those are real and the wallet-SDK browser code uses them for live UX. But every YF backend integration (Python SDK, shell scripts, operator) uses REST polling of /api/users/{user_id}/messages/{message_id} — simpler, no missed-event race, no streaming state to manage. Match the Python SDK's poll_message_completion: 2s interval, 300s ceiling, terminal when executed is set.
Pitfall
Calling <payments-host>/api/mq/submit to mint obligations, send payments, etc.
Avoid
That endpoint is service-to-service only and needs a vault-equipped caller (you'll get 403 Insufficient permissions: CryptoOperations otherwise). Use payments-direct GraphQL mutations (createObligation, acceptObligation, instant, …) — they call into the same pipeline with proper input normalisation and entity resolution.
Pitfall
Hard-coding addresses / contract addresses on the client
Avoid
Prefer resolver-managed fields and wallet/entity IDs. If your app truly needs deployment constants, load them from your deployment config or an approved backend adapter, not from browser code.
Pitfall
Storing a refresh token on the client and using it twice
Avoid
Refresh is single-use. Each refresh returns a new refresh token; replace the stored one atomically.
Pitfall
Showing the user a stale balance after a payment completes
Avoid
After Completed status, balance state may take a few seconds to converge in the consumer pipeline. Re-query payments after a 2-3s delay.
Pitfall
Trying to act as a group via an HTTP header
Avoid
Delegation is a JWT claim (acting_as), not a header. Mint a delegation JWT via POST /auth/delegation/jwt.
Pitfall
Using a service token from client code
Avoid
Service tokens (payments-service audience) are for service-to-service calls only. Client apps use user tokens.

"Help, I want to build X"

You want to build…
A login/signup screen
Read this
Auth quickstart: /docs/api/auth/guides/getting-started.md
You want to build…
A payment-sending UI
Read this
Cross-service walkthrough steps 3-4
You want to build…
A chat-with-AI UI
Read this
agents-and-workspaces.md — threads + streaming sections
You want to build…
A deal-flow approval UI
Read this
dms.md and the obligation operations in /docs/api/payments
You want to build…
A periodic / amortizing loan deal (disburse principal, then split each repayment into interest + principal)
Read this
loans.md — the app-built loan deal (compose → sign → activate → live periodic repayment), sibling of the one-shot rental deal
You want to build…
A wallet-management UI
Read this
The @yieldfabric/wallet SDK (drop-in React components)
You want to build…
An onboarding T&C / agreement-acceptance flow (zero-payment obligation, user as counterpart)
Read this
Use createObligation with counterpart set to the same user and no initialPayments. The on-chain safeMint auto-accepts this shape in a single call — ObligationCreated and ObligationAccepted fire in the same receipt and the contract lands at COMPLETED immediately. No follow-up acceptObligation needed.
You want to build…
A custom AI agent with tools
Read this
You want to build…
A confidential escrow released by a neutral third party's attestation
Read this
dms.md's doc_escrow template + . A payer escrows funds; a neutral attestor commits a zero-knowledge document; the beneficiary releases via a doc-gated data policy — composing commit_oracle_document, add_data_policy, and execute_under_policy.

In one sentence

Get a JWT from auth.yieldfabric.com/auth/login; use api.yieldfabric.com/graphql for federated reads, the JWT-selected payments /graphql for chain-bound mutations, that same payments host's /api/users/.../messages to poll settlement, and agents.yieldfabric.com for agents REST/SSE. Omit idempotencyKey or use a fresh UUIDv4 per logical mutation. Switch on errors[].extensions.code (GraphQL) or HTTP status (REST). Treat refresh tokens as single-use.

That's the platform.