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
Message status
URL
pay.yieldfabric.com/api/users/{id}/messages/{id}
What it's for
After a mutation that submits on-chain work, poll this REST endpoint to learn when the operation actually settled.
Surface
GraphQL gateway
URL
api.yieldfabric.com/graphql
What it's for
All public GraphQL reads and mutations — entities, wallets, contracts, activity feeds, payment listings, deal-flow reads, instant, deposits, withdrawals, obligations, swaps, and composed payment operations.
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: api.yieldfabric.com/graphql for every GraphQL operation, auth REST for sign-in, payments REST for settlement polling, 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
All public GraphQL reads and mutations: entity lookups, wallet listings, contracts, activity feeds, payment listings, deal reads, payments, obligations, swaps, deposits, and withdrawals.
Public surface
GET https://pay.yieldfabric.com/api/users/{user_id}/messages/{message_id} (payments REST)
For
Poll the message returned by an on-chain mutation until executed is non-null.
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 public GraphQL gateway mutation, then poll the message-status endpoint.

Internal surface — do not use from app code
pay.yieldfabric.com/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 the public GraphQL gateway mutations instead.
Internal surface — do not use from app code
pay.yieldfabric.com/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": "…" }
}

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:

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

Single-use refresh. If you get a 401 here, the user must re-sign-in.

2. Send a payment

curl -X POST https://api.yieldfabric.com/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.

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 public GraphQL gateway.

# 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 https://api.yieldfabric.com/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" \
  "https://pay.yieldfabric.com/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 https://api.yieldfabric.com/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" \
  "https://pay.yieldfabric.com/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, notional, 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.

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 https://api.yieldfabric.com/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 "https://pay.yieldfabric.com/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.

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 — public reads and mutations, including on-chain mutations (instant, obligations, swaps, deposits, withdrawals) and cross-subgraph reads.
URL
https://agents.yieldfabric.com/**
Use
Agents REST (working groups, threads, workflows, KGs, conversation)
URL
https://pay.yieldfabric.com/api/users/{user_id}/messages*
Use
Payments messages REST (poll status of an on-chain submission)

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
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
Routing GraphQL through service hosts
Avoid
Use api.yieldfabric.com/graphql for every public GraphQL operation. Service hosts are REST/SSE surfaces.
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 pay.yieldfabric.com/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 the public GraphQL gateway 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 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

In one sentence

Get a JWT from auth.yieldfabric.com/auth/login; use api.yieldfabric.com/graphql for all public GraphQL reads and mutations, pay.yieldfabric.com/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.

YieldFabric docs(317)