AI prompt for a personal app
Many customers now use an AI coding assistant to write their first tastytrade API
client. The API has a few requirements that are easy to miss: a User-Agent on every
request, access tokens that last 15 minutes, dasherized JSON keys, and a dry-run
before any order. This page gives you a prompt that already contains those details,
so the assistant starts from what the API documents instead of from guesses.
The prompt builds a small, read-only app for your own account. It does not build a trading app. It lists accounts, shows balances and positions, and downloads transaction history. Order JSON for a few standard strategies is included as reference only.
Before you paste it
- Create an OAuth application and a personal grant, and keep the client secret and refresh token to yourself. See OAuth2 authentication. Sandbox and production are separate applications with separate credentials.
- Request only the scopes you need. Every call in this prompt is a read, and
reads need only the
readscope. - Set the credentials as environment variables in your own terminal. Do not paste them into the assistant's chat.
- Start in the sandbox. Sandbox balances, positions and transactions are synthetic and reset every 24 hours, so they only show you that the code works.
Disclosures: connecting an AI system to your account is subject to the tastytrade MCP Server Disclosures.
The prompt
Copy everything in the block below. Where it says "I", it means you.
You are helping me build a small PERSONAL command-line app for my own tastytrade
account. It is READ-ONLY: it signs in with OAuth2, lists my accounts, shows balances
and positions, and downloads transaction history. It is NOT a trading app.
Use only the facts in this prompt. Do not rely on your memory of other tastytrade or
tastyworks API examples; some describe a retired username/password login
(POST /sessions) that no longer works. If you need an endpoint, field or behaviour
that is not listed here, tell me and ask rather than guessing. The API reference is
at https://developer.tastytrade.com/reference and an index for AI tools is at
https://developer.tastytrade.com/llms.txt.
If I have not said which language to use, ask me once, then continue.
== SCOPE ==
Build exactly this and nothing more:
1. OAuth2 sign-in with a refresh token, and automatic access-token refresh.
2. List my accounts.
3. For an account I choose: show balances and show positions.
4. Download transaction history with pagination and an optional date range, printed
as a table and optionally written to a CSV file.
Do NOT build: order entry or management, streaming, market data, strategy or
automation logic, scheduled jobs, a web UI, or a hosted service. Do not add features
I did not ask for.
== ENVIRONMENT AND SECRETS ==
- Base URL comes from the env var TASTYTRADE_BASE_URL. The default is the SANDBOX:
https://api.cert.tastyworks.com
Production is https://api.tastyworks.com. Never default to production. I will
switch to it myself, only after the sandbox flow works. Print the base URL at every
startup so I can see which environment I am using.
- Sandbox and production use separate OAuth applications. Credentials from one do
not work on the other, and the failure shows up as the error code
invalid_credentials.
- Read credentials ONLY from env vars: TASTYTRADE_CLIENT_SECRET and
TASTYTRADE_REFRESH_TOKEN. TASTYTRADE_CLIENT_ID is optional. If you support a .env
file, add .env to .gitignore and create only a .env.example with placeholder text.
- Never hard-code a secret, token or account number, including in tests, examples
and the README. Never log or print the client secret, refresh token or access
token, including in error messages, exception traces and debug output of requests.
Redact the Authorization header if you log requests.
- Do not ask me to paste secrets into this chat. If an env var is missing, stop with
a clear message that names the variable.
- Use OAuth2 only. Never ask for or store my tastytrade username or password.
== AUTHENTICATION: OAuth2 REFRESH-TOKEN FLOW ==
- To get an access token: POST {base}/oauth/token with Content-Type
application/json and a JSON body containing grant_type set to "refresh_token",
refresh_token, and client_secret. client_id is optional; if you send it, it must
match my application.
- The response contains access_token. Send it on every other request as the header
Authorization: Bearer {access_token}
- An access token is valid for 15 minutes and cannot be extended.
- Refreshing does NOT return a new refresh token. Keep using the one from the env
var, and do not overwrite or rotate it.
- Keep the access token in memory only. Record when you received it, using a
monotonic clock. Refresh before it expires: get a new token when the current one is
more than 10 minutes old. Do not refresh before every request.
- If a request returns 401 with a JSON body, refresh once and retry that one request
once (not an order request). If it still returns 401, stop and report it.
- Never retry a failed token request in a loop. Repeated failed logins can get my IP
address blocked for about 8 hours. On any failure from /oauth/token, stop and show
the HTTP status and error.code only.
== EVERY REQUEST ==
- Send a User-Agent header in the form product/version, for example
my-portfolio-viewer/1.0, on EVERY request including /oauth/token. Without it the
API returns 401 even with a valid token, and the body is an HTML page, not JSON.
- Send Accept: application/json and set a timeout on every request.
- JSON keys are dasherized (account-number, net-liquidating-value). Use them exactly
as returned.
- List endpoints return the array in data.items. The balances endpoint returns one
object in data.
- Money amounts and quantities are JSON strings such as "100" or "27.74". Parse them
with a decimal type, never a binary float. Amounts come with a separate field
ending in -effect whose value is Credit, Debit or None. Keep that field next to the
amount when you display it.
== ENDPOINTS (these are the only ones you need) ==
1. GET /customers/me/accounts
Use the literal text "me". data.items is a list; each item has an "account"
object (account-number, nickname, account-type-name, margin-or-cash, is-closed)
and an "authority-level" (owner, trade-only or read-only). If I have more than one
account, let me choose; do not assume the first.
2. GET /accounts/{account_number}/balances
data is one object. Show cash-balance, net-liquidating-value,
equity-buying-power, derivative-buying-power, maintenance-requirement,
cash-available-to-withdraw and pending-cash.
3. GET /accounts/{account_number}/positions
data.items is a list of open positions. Show symbol, instrument-type,
underlying-symbol, quantity, quantity-direction (Long, Short or Zero),
average-open-price, close-price and multiplier. Do not assume quantity is signed;
use quantity-direction for the side. This endpoint returns no live profit/loss and no
mark unless include-marks=true is set. Do not compute or show profit/loss.
4. GET /accounts/{account_number}/transactions
Query parameters: page-offset (starts at 0), per-page (use 250), sort (Desc is
newest first and is the default; Asc is oldest first), start-date and end-date
(YYYY-MM-DD; end-date defaults to now). Optional filters: symbol,
instrument-type, and types[] (repeat the key for several values).
The response has data.items and a top-level pagination object with per-page,
page-offset, item-offset, total-items, total-pages and current-item-count.
Paging loop: start at page-offset=0 and keep per-page the same. Process data.items.
Stop when page-offset + 1 >= pagination.total-pages. If there is no pagination
object, stop when data.items is empty or has fewer than per-page entries.
Otherwise add 1 to page-offset and fetch the next page, pausing briefly between
pages.
Show id, transaction-type, transaction-sub-type, description, symbol, action,
quantity, price, value, value-effect, net-value, net-value-effect, executed-at and
transaction-date.
== ERRORS ==
Error bodies look like {"error": {"code": "...", "message": "..."}}. Branch on
error.code, never on the message text. Show me the code and message.
- 400: a bad parameter. Do not retry unchanged.
- 401: see AUTHENTICATION. A 401 with an HTML body and no error.code means the
User-Agent header is missing or malformed: fix the header, do not retry.
error.code invalid_credentials usually means the credentials belong to the other
environment: stop and tell me.
- 403: the token's scope or the account's authority-level does not allow it. Do not
retry.
- 404: wrong account number, or wrong environment. Do not retry.
- 422: the request is invalid in context. Read error.message and error.errors[], fix
the cause, do not retry unchanged.
- 429: too many requests. No rate-limit headers are documented, so do not depend on
one. Back off exponentially with random jitter (about 1s, 2s, 4s, 8s), at most 5
attempts (not for order requests), then stop. Never retry in a tight loop.
- 5xx and network errors: retry GET requests with the same backoff. Retry nothing
else.
== ORDERS: OUT OF SCOPE BY DEFAULT ==
This app is read-only. Do NOT write any code that places, replaces or cancels an
order unless I explicitly ask for it in a later message. If I do ask, build the
smallest thing that works and follow every rule here:
a. Use the sandbox unless I explicitly say production. State the environment
before any order action.
b. First POST the order body to /accounts/{account_number}/orders/dry-run. It runs
the preflight checks without routing the order and returns 201. Show me
buying-power-effect, fee-calculation and warnings. Warnings are informational
notices in data.warnings[] of a successful dry-run and do not block the order,
but some indicate the order would be rejected if routed live. Treat ANY warning
as a stop and show it to me. A dry-run that fails preflight is a 422; show me
error.message and error.errors[].
c. Only after I have seen the dry-run output, ask me to type a confirmation that
names this exact order (symbol, quantity, price). Then POST the identical body to
/accounts/{account_number}/orders. Never submit from a loop, a schedule, a
default or a --yes flag.
d. Put a unique external-identifier (for example a UUID) in every order body. The API
has no idempotency-key header and does NOT deduplicate retried submissions. Never
retry an order request automatically. If a submit times out or returns 5xx, do not
resubmit: GET /accounts/{account_number}/orders (or
/accounts/{account_number}/orders/live) and look for that external-identifier
first.
e. Support only the three examples below. No other strategies, no complex or
bracket orders, no futures, no cryptocurrency. Do not invent fields.
The examples below are shapes, not trades. Replace the symbols, quantities and prices
with values I give you. The option symbols below are from past expirations: take real
ones from GET /option-chains/{symbol}/nested and copy them exactly, spaces included.
For multi-leg orders, price is the net amount across all legs; price-effect is Debit
if I pay and Credit if I receive. Keys are dasherized.
Equity, limit buy of 100 shares:
{
"time-in-force": "Day",
"order-type": "Limit",
"price": "175.25",
"price-effect": "Debit",
"source": "my-api-code",
"external-identifier": "aapl-buy-2026-06-09-0001",
"legs": [
{
"instrument-type": "Equity",
"symbol": "AAPL",
"quantity": 100,
"action": "Buy to Open"
}
]
}
Vertical spread (a bear call vertical for a net credit):
{
"time-in-force": "Day",
"order-type": "Limit",
"price": "0.85",
"price-effect": "Credit",
"legs": [
{
"instrument-type": "Equity Option",
"symbol": "AAPL 221118C00155000",
"quantity": 1,
"action": "Sell to Open"
},
{
"instrument-type": "Equity Option",
"symbol": "AAPL 221118C00157500",
"quantity": 1,
"action": "Buy to Open"
}
]
}
Iron condor (four legs, net credit):
{
"source": "my-api-code",
"order-type": "Limit",
"time-in-force": "Day",
"price": "1.51",
"price-effect": "Credit",
"legs": [
{
"instrument-type": "Equity Option",
"symbol": "TSLA 230714P00210000",
"action": "Buy to Open",
"quantity": 1
},
{
"instrument-type": "Equity Option",
"symbol": "TSLA 230714P00215000",
"action": "Sell to Open",
"quantity": 1
},
{
"instrument-type": "Equity Option",
"symbol": "TSLA 230714C00282500",
"action": "Sell to Open",
"quantity": 1
},
{
"instrument-type": "Equity Option",
"symbol": "TSLA 230714C00290000",
"action": "Buy to Open",
"quantity": 1
}
]
}
For each of these, add your own unique external-identifier before the dry-run.
== DELIVERABLES ==
- A few small files and a README with setup steps that use placeholder values only.
- Tests, if any, use fake values and a mocked HTTP layer, never my real credentials.
- Before you finish, check your own code: no hard-coded secrets, a User-Agent on
every request including the token request, the sandbox as the default base URL,
and no order code unless I asked for it. Then tell me plainly what you could not
verify.
After the assistant answers
Check the generated code against this list before you run it against anything but the sandbox.
- Secrets are read from environment variables. Search the code for your client
secret and refresh token, and for any
printor log call near them. - Every request, including the token request, sends a
User-Agent. - The default base URL is
https://api.cert.tastyworks.com. - The access token is refreshed before 15 minutes and the refresh token is never overwritten.
- The transaction loop stops on
pagination.total-pages, and a429backs off instead of retrying at once. - There is no code that places an order, unless you asked for one.
To use production, set TASTYTRADE_BASE_URL to https://api.tastyworks.com and use
the production application's credentials. Do not change anything else at the same
time.
Next steps
- Agent Quickstart — the raw HTTP path through a sandbox trade.
- Pagination and filtering — the transaction filters in detail.
- Error handling — what each status means and which to retry.
- MCP server — an alternative to writing your own client.