Build / Your interface on our model

Vibe code the app
you wish you had.

A net worth dashboard. A private asset tracker. A household view for your phone. Give your coding agent the contract.

Start with a prompt

Paste this.
Make it yours.

The guide contains the complete app protocol and all 32 tool descriptions. Your app is an OAuth public client with PKCE, exactly as an agent is.

Starter prompt · copy into your coding agent
Build me a net worth dashboard on Sightline. Read https://www.sightlineapp.com/llms-full.txt first. Use OAuth with PKCE and the public MCP tools at https://my.sightlineapp.com/mcp. Ask for read-only access. Read get_started_with_sightline first. Show net worth history, account balances, and holdings. Preserve decimal strings. Show the scope, dates, missing values, stale data, and estimates. Label a restricted total as a subtotal. Keep browser tokens in memory and out of URLs and logs. Calculate returns in the app.

No SDK. No separate API. No helper endpoint. Use the public MCP tools. Read llms-full.txt before implementation.

The real protocol

A public client.
One explicit grant.

Discover the endpoints. Register the app. Use PKCE. The human approves the household and scope on the control page.

Discover & register

Read the protected resource metadata. Then read the authorization server metadata. Use the endpoint URLs from those documents.

Send the registration JSON to POST /oauth/register. Keep the returned client_id and redirect address. A public client has no client secret.

An HTTPS Client ID Metadata Document can also supply the client ID. Its client_id must equal its own URL.

Discovery documents
https://my.sightlineapp.com/.well-known/oauth-protected-resource/mcp
https://my.sightlineapp.com/.well-known/oauth-authorization-server
POST /oauth/register
{
  "client_name": "Example app",
  "redirect_uris": [
    "https://app.example/callback"
  ],
  "token_endpoint_auth_method": "none",
  "grant_types": [
    "authorization_code",
    "refresh_token"
  ],
  "response_types": [
    "code"
  ]
}

Register the exact callback. HTTPS redirects need a host. HTTP permits localhost and loopback addresses only. Native apps use a reverse-domain scheme.

Create the PKCE request

Create a random verifier with 43 to 128 permitted characters. Calculate its SHA-256 digest. Encode the digest with base64url, without padding.

Create a separate random state. Keep the verifier and state until the callback. Open the authorization URL in the browser.

Authorization query parameters
ParameterValue
response_typecode
client_idThe registered client ID
redirect_uriThe exact registered address
code_challengeThe base64url SHA-256 digest
code_challenge_methodS256
resourcehttps://my.sightlineapp.com/mcp
stateA separate random state value

The human signs in, selects a household, and approves a scope. Read only is the first selection when it fits the member scope.

Exchange the code

Check the callback route, state, and issuer (iss). If the callback contains an error, stop the connection.

Send a form body to POST /oauth/token. Use application/x-www-form-urlencoded. URL-encode each value.

The code expires after 60 seconds and works one time. The access token lasts one hour.

POST /oauth/token · form fields
grant_type=authorization_code
client_id=<client_id>
code=<callback_code>
code_verifier=<saved_verifier>
redirect_uri=<exact_registered_address>
resource=https://my.sightlineapp.com/mcp

The response contains access_token, refresh_token, token_type, and expires_in. Keep browser tokens in memory. Keep device tokens in the Keychain.

Initialize MCP

Send initialize to POST /mcp. Then send notifications/initialized. The notification has no ID and returns status 202.

Send tools/list to read the permitted tool schemas. Read get_started_with_sightline before account setup.

The service returns JSON without server sessions. Each request uses the bearer token and protocol headers.

POST /mcp · initialize
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": {
      "name": "Example app",
      "version": "1.0.0"
    }
  }
}
POST /mcp · initialized notification
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Call a tool

After initialization, send a JSON-RPC tools/call request. The bearer token below is a placeholder.

Check the JSON-RPC error first. Check result.isError before you read result.structuredContent. A tool error can occur with HTTP 200.

The full result also contains the same JSON as text in content. Keep decimal strings exact.

curl · tool call after initialization
curl https://my.sightlineapp.com/mcp \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  --data '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_net_worth","arguments":{}}}'

Inspect the wire

Requests you can read.
Results you can inspect.

Each result is a real result of the service for a synthetic demo household. One long text field is shortened. No example contains real personal data.

get_net_worth

Read a current total

The request needs no arguments. The result gives the total with its date, its scope, and its limits: complete, stale_accounts, and missing. Read them before a calculation.

Request · get_net_worth
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "get_net_worth",
    "arguments": {}
  }
}
Result · demo household
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "isError": false,
    "structuredContent": {
      "as_of": "2026-11-14",
      "currency": "CAD",
      "net_worth": "1304821.24",
      "assets": "1719731.64",
      "liabilities": "414910.40",
      "is_full_household": true,
      "complete": true,
      "stale_accounts": [],
      "missing": [],
      "scope": {
        "entities": "all",
        "account_types": "all",
        "detail": "full",
        "access": "read"
      },
      "history": [],
      "estimated_holdings": [],
      "estimated_count": 0,
      "derived_count": 0,
      "filters": {
        "as_of": null,
        "from": null,
        "to": null,
        "interval": "day",
        "include_entities": false,
        "include_components": false,
        "limit": 100,
        "cursor": null,
        "tax_entity_ids": null,
        "tax_entity_id": null,
        "account_types": null,
        "asset_groups": null
      },
      "not_started_count": 0,
      "entities": [
        {
          "tax_entity_id": "ent_4dcb0b7902a940e4a1f53ad0d8d8acb7",
          "type": "person",
          "direct": {
            "amount": "1304821.24",
            "currency": "CAD"
          },
          "direct_in_household_currency": "1304821.24",
          "household_fraction": "1",
          "household_part": "1304821.24",
          "notes": []
        }
      ],
      "accounts": [
        {
          "account_id": "acct_10dbf3599a5e4300b1e133da15f90c0f",
          "valuation_mode": "holdings",
          "value": {
            "amount": "97420.00",
            "currency": "CAD",
            "as_of": "2026-11-14",
            "source": "holdings",
            "stale": false
          },
          "total": null,
          "holdings_value": "97420.00",
          "unclassified": null,
          "difference": null,
          "notes": []
        }
      ],
      "excluded_entities": [],
      "excluded_unclassified": [],
      "notes": [],
      "components": [],
      "next_cursor": null,
      "recalculation_pending": false
    }
  }
}
get_net_worth · history

Read daily history

Use from and to together. The maximum is 366 inclusive days. Each row has the totals of that day and the counts of stale, missing, and estimated values.

Request · get_net_worth
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "get_net_worth",
    "arguments": {
      "from": "2026-11-09",
      "to": "2026-11-13",
      "interval": "day"
    }
  }
}
Result · demo household
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "isError": false,
    "structuredContent": {
      "as_of": null,
      "currency": "CAD",
      "net_worth": null,
      "assets": null,
      "liabilities": null,
      "is_full_household": true,
      "complete": true,
      "stale_accounts": [],
      "missing": [],
      "scope": {
        "entities": "all",
        "account_types": "all",
        "detail": "full",
        "access": "read"
      },
      "history": [
        {
          "estimated_count": 0,
          "derived_count": 0,
          "as_of": "2026-11-09",
          "net_worth": "1300990.15",
          "assets": "1715900.55",
          "liabilities": "414910.40",
          "complete": true,
          "stale_count": 0,
          "missing_count": 0,
          "not_started_count": 0
        },
        {
          "estimated_count": 0,
          "derived_count": 0,
          "as_of": "2026-11-10",
          "net_worth": "1301167.80",
          "assets": "1716078.20",
          "liabilities": "414910.40",
          "complete": true,
          "stale_count": 0,
          "missing_count": 0,
          "not_started_count": 0
        },
        {
          "estimated_count": 0,
          "derived_count": 0,
          "as_of": "2026-11-11",
          "net_worth": "1302136.15",
          "assets": "1717046.55",
          "liabilities": "414910.40",
          "complete": true,
          "stale_count": 0,
          "missing_count": 0,
          "not_started_count": 0
        },
        {
          "estimated_count": 0,
          "derived_count": 0,
          "as_of": "2026-11-12",
          "net_worth": "1302853.50",
          "assets": "1717763.90",
          "liabilities": "414910.40",
          "complete": true,
          "stale_count": 0,
          "missing_count": 0,
          "not_started_count": 0
        },
        {
          "estimated_count": 0,
          "derived_count": 0,
          "as_of": "2026-11-13",
          "net_worth": "1304821.24",
          "assets": "1719731.64",
          "liabilities": "414910.40",
          "complete": true,
          "stale_count": 0,
          "missing_count": 0,
          "not_started_count": 0
        }
      ],
      "estimated_holdings": [],
      "estimated_count": 0,
      "derived_count": 0,
      "filters": {
        "as_of": null,
        "from": "2026-11-09",
        "to": "2026-11-13",
        "interval": "day",
        "include_entities": false,
        "include_components": false,
        "limit": 100,
        "cursor": null,
        "tax_entity_ids": null,
        "tax_entity_id": null,
        "account_types": null,
        "asset_groups": null
      },
      "not_started_count": 0,
      "entities": [],
      "accounts": [],
      "excluded_entities": [],
      "excluded_unclassified": [],
      "notes": [],
      "components": [],
      "next_cursor": null,
      "recalculation_pending": false
    }
  }
}
list_holdings

Read holdings and prices

Each holding has a quantity, a price with its date and source, and a value. The account states its valuation mode, and the result states its scope and completeness.

Request · list_holdings
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "list_holdings",
    "arguments": {
      "as_of": "2026-11-13"
    }
  }
}
Result · demo household
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "isError": false,
    "structuredContent": {
      "accounts": [
        {
          "account_id": "acct_10dbf3599a5e4300b1e133da15f90c0f",
          "valuation_mode": "holdings",
          "total": null,
          "holdings_value": "97420.00",
          "unclassified": null,
          "difference": null,
          "holdings": [
            {
              "recorded": {
                "account_holding_id": "hold_10e3c458f18e4cf5af030099ad82eaf2",
                "as_of": "2026-11-09",
                "quantity": "5000.00"
              },
              "price_source": "market",
              "instrument_id": "inst_87582f0086b14560847745850f66850f",
              "symbol": "CASH:CAD",
              "quantity": "5000.00",
              "price": {
                "amount": "1",
                "currency": "CAD",
                "as_of": "2026-11-13",
                "source": "identity"
              },
              "value": "5000.00",
              "stale": false,
              "rate": "1",
              "rate_date": "2026-11-13",
              "reason": null,
              "asset_group": "cash"
            },
            {
              "recorded": {
                "account_holding_id": "hold_6c080125ca354216b64c82c8cf87ddff",
                "as_of": "2026-11-09",
                "quantity": "340"
              },
              "price_source": "market",
              "instrument_id": "inst_92607e437c9f4fb79cb705cc99bf8379",
              "symbol": "VFV.TO",
              "quantity": "340",
              "price": {
                "amount": "151.40",
                "currency": "CAD",
                "as_of": "2026-11-13",
                "source": "demo"
              },
              "value": "51476.00",
              "stale": false,
              "rate": "1",
              "rate_date": "2026-11-13",
              "reason": null,
              "asset_group": "public"
            },
            {
              "recorded": {
                "account_holding_id": "hold_68ee805db20f4537a8b7fb89fa205776",
                "as_of": "2026-11-09",
                "quantity": "1200"
              },
              "price_source": "market",
              "instrument_id": "inst_baec19abcddf4ba087964e6642ce8c8f",
              "symbol": "XEQT.TO",
              "quantity": "1200",
              "price": {
                "amount": "34.12",
                "currency": "CAD",
                "as_of": "2026-11-13",
                "source": "demo"
              },
              "value": "40944.00",
              "stale": false,
              "rate": "1",
              "rate_date": "2026-11-13",
              "reason": null,
              "asset_group": "public"
            }
          ],
          "notes": []
        }
      ],
      "estimated_holdings": [],
      "next_cursor": null,
      "scope": {
        "entities": "all",
        "account_types": "all",
        "detail": "full",
        "access": "read"
      },
      "is_full_household": true,
      "complete": true,
      "stale_accounts": [],
      "missing": []
    }
  }
}
create_transactions

Write a batch with a transfer pair

The shared transfer_key joins the two new transfer rows. One atomic write accepts up to 200 rows. The result returns each stored row. The two transfer rows share a transfer_id.

Request · create_transactions
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "create_transactions",
    "arguments": {
      "transactions": [
        {
          "account_id": "acct_66ce0bea4b7e41b8b7e340fbeb1914b5",
          "date": "2026-11-13",
          "amount": "-86.40",
          "currency": "CAD",
          "type": "expense",
          "category": "groceries",
          "merchant": "Corner Market",
          "external_id": "chq-2026-11-13-001"
        },
        {
          "account_id": "acct_66ce0bea4b7e41b8b7e340fbeb1914b5",
          "date": "2026-11-13",
          "amount": "-500.00",
          "currency": "CAD",
          "type": "transfer",
          "transfer_key": "to-savings",
          "external_id": "chq-2026-11-13-002"
        },
        {
          "account_id": "acct_b581a6bd50c94294a73d22264e63efb0",
          "date": "2026-11-13",
          "amount": "500.00",
          "currency": "CAD",
          "type": "transfer",
          "transfer_key": "to-savings",
          "external_id": "sav-2026-11-13-001"
        }
      ]
    }
  }
}
Result · demo household
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "isError": false,
    "structuredContent": {
      "transactions": [
        {
          "id": "txn_e683f613d86c47558cd8ab28a09f5f37",
          "account_id": "acct_66ce0bea4b7e41b8b7e340fbeb1914b5",
          "tax_entity_id": null,
          "external_id": "chq-2026-11-13-001",
          "date": "2026-11-13",
          "amount": "-86.40",
          "currency": "CAD",
          "type": "expense",
          "pending": false,
          "excluded": false,
          "category": "groceries",
          "income_type": null,
          "income_source_id": null,
          "transfer_id": null,
          "description": null,
          "merchant": "Corner Market",
          "instrument_id": null,
          "quantity": null,
          "unit_price": null,
          "cost_basis": null,
          "source": "agent",
          "created_at": "2026-11-14T13:44:26.318434-05:00",
          "updated_at": "2026-11-14T13:44:26.318434-05:00",
          "other_account_ids": []
        },
        {
          "id": "txn_8a0884242fd142ea9063ad12abc05f98",
          "account_id": "acct_66ce0bea4b7e41b8b7e340fbeb1914b5",
          "tax_entity_id": null,
          "external_id": "chq-2026-11-13-002",
          "date": "2026-11-13",
          "amount": "-500.00",
          "currency": "CAD",
          "type": "transfer",
          "pending": false,
          "excluded": false,
          "category": null,
          "income_type": null,
          "income_source_id": null,
          "transfer_id": "xfer_9279c3b223424d05b2d0678439fda8d3",
          "description": null,
          "merchant": null,
          "instrument_id": null,
          "quantity": null,
          "unit_price": null,
          "cost_basis": null,
          "source": "agent",
          "created_at": "2026-11-14T13:44:26.318434-05:00",
          "updated_at": "2026-11-14T13:44:26.318434-05:00",
          "other_account_ids": [
            "acct_b581a6bd50c94294a73d22264e63efb0"
          ]
        },
        {
          "id": "txn_e16c9176fb5c48fe89dd233428563db5",
          "account_id": "acct_b581a6bd50c94294a73d22264e63efb0",
          "tax_entity_id": null,
          "external_id": "sav-2026-11-13-001",
          "date": "2026-11-13",
          "amount": "500.00",
          "currency": "CAD",
          "type": "transfer",
          "pending": false,
          "excluded": false,
          "category": null,
          "income_type": null,
          "income_source_id": null,
          "transfer_id": "xfer_9279c3b223424d05b2d0678439fda8d3",
          "description": null,
          "merchant": null,
          "instrument_id": null,
          "quantity": null,
          "unit_price": null,
          "cost_basis": null,
          "source": "agent",
          "created_at": "2026-11-14T13:44:26.318434-05:00",
          "updated_at": "2026-11-14T13:44:26.318434-05:00",
          "other_account_ids": [
            "acct_66ce0bea4b7e41b8b7e340fbeb1914b5"
          ]
        }
      ],
      "next_cursor": null
    }
  }
}
record_holding

Record an end-of-day quantity

A recorded quantity is a state, not a trade. Use cash_currency instead of instrument_id for cash. For repeated writes, add a stable external_id and repeat the same content.

Request · record_holding
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "record_holding",
    "arguments": {
      "account_id": "acct_10dbf3599a5e4300b1e133da15f90c0f",
      "instrument_id": "inst_92607e437c9f4fb79cb705cc99bf8379",
      "quantity": "340",
      "as_of": "2026-11-09",
      "cost_basis": "44200.00",
      "external_id": "brokerage-vfv-opening"
    }
  }
}
Result · demo household
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "isError": false,
    "structuredContent": {
      "id": "hold_6c080125ca354216b64c82c8cf87ddff",
      "account_id": "acct_10dbf3599a5e4300b1e133da15f90c0f",
      "instrument_id": "inst_92607e437c9f4fb79cb705cc99bf8379",
      "as_of": "2026-11-09",
      "quantity": "340",
      "cost_basis": "44200.00",
      "source": "agent",
      "observed_at": "2026-11-14T13:44:26.303745-05:00",
      "external_id": "brokerage-vfv-opening"
    }
  }
}
get_started_with_sightline

Read the household and grant first

The first call of an agent. It returns the setup skill, the household state, and the scope of the grant. Follow the skill before account setup.

Request · get_started_with_sightline
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_started_with_sightline",
    "arguments": {}
  }
}
Result · demo household
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "isError": false,
    "structuredContent": {
      "household": {
        "id": "hh_94ca7dd7b76845f1aa785bd54b2db3a1",
        "currency": "CAD",
        "tax_entities": 1,
        "accounts": 7,
        "bank_connections": 0,
        "as_of": "2026-11-14",
        "stale_accounts": [],
        "missing": []
      },
      "grant": {
        "entities": "all",
        "account_types": "all",
        "detail": "full",
        "access": "read"
      },
      "simple_path": [
        "get_started_with_sightline",
        "get_household",
        "list_accounts",
        "create_account",
        "record_account_value",
        "get_net_worth"
      ],
      "skill": {
        "name": "get_started_with_sightline",
        "version": "1.4.3",
        "text": "(shortened here: the setup guide for the agent, 231 lines of Markdown)"
      }
    }
  }
}

Read every page

Keep the cursor opaque.

Start without a cursor. Read next_cursor. Repeat with the same filters and limit until it is null.

Do not decode or change a cursor. The maximum page limit is 500. The same method applies to other tools with limit and cursor inputs.

Next page · use the returned cursor
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "list_accounts",
    "arguments": {
      "limit": 100,
      "cursor": "<next_cursor>"
    }
  }
}

Token lifecycle

Rotate. Replace.
Never reuse.

Use each refresh token one time. Replace both saved tokens before the next request. Serialize refresh requests. A second use revokes the grant.

A refresh token expires after 90 days without use. If the response is lost, start authorization again.

On HTTP 401

Read WWW-Authenticate. If the access token expired, refresh once. Retry once. If refresh gives invalid_grant or the retry gives 401, start authorization again.

POST /oauth/token · refresh form
grant_type=refresh_token
client_id=<client_id>
refresh_token=<current_refresh_token>
resource=https://my.sightlineapp.com/mcp
POST /oauth/revoke · form fields
client_id=<client_id>
token=<current_refresh_token>
token_type_hint=refresh_token

For writes, use a stable external_id when the tool accepts it. Repeat the same ID and content after a timeout. A timeout does not prove that a write failed.

Bounded requests

Build within the limits.

Use Retry-After on HTTP 429. Keep requests small enough to complete within the deadline.

120tool calls / grant / 60 seconds
20 stool request deadline
366inclusive days / history request
200transaction rows / atomic batch

The MCP body limit is 1 MiB. The default body limit is 64 KiB. A body read has a 10-second deadline. Read the complete guide for all limits and the browser TypeScript example.