# compute-day-count-to-settlement

A DAP Foundry tool for AI agents. Give a trade date, a settlement lag and an ISDA day count convention. Get the settlement date on the Payments Canada calendar and the exact fraction. Every call states how old the calendar may be. Stale data is refused, never guessed.

We attest what we collected and when we last looked. We do not issue rulings.

This file also describes the JSON endpoints of three text tools, each in its own section after the day count's: convert-unix-timestamp-to-date, decode-base64-to-text and diff-two-line-lists. What a text tool endpoint answers is under Text tool endpoints, just before them. Each takes one parameter, text, and reads no calendar and no clock. Last, under MCP server, it describes the MCP server at /mcp, whose tools are these four endpoints.

## Endpoint

- URL: https://foundry.dap.solutions/api/compute-day-count-to-settlement
- Surface: /api/compute-day-count-to-settlement, a stateless Cloudflare Pages Function.
- Methods: GET with query parameters. POST with a JSON object as the body, any content-type. Each needs a payment (see Payment). HEAD returns 402 with the price and no body. OPTIONS returns 204 for a CORS preflight.
- Size: at most 4,096 bytes of UTF-8, counted over the query string (without the leading ?) or the raw bytes of the body. A content-length header is optional. A POST whose content-length is over 4,096 is refused without reading the body. Otherwise the body is read in chunks and the read stops as soon as it passes 4,096 bytes. A body that is not valid UTF-8 is unreadable.
- Depth: values may nest at most 8 levels deep, counting the top level object as level 1. Every valid input is a plain value, so this limit refuses nothing valid.
- Page: https://foundry.dap.solutions/compute-day-count-to-settlement/ runs the same core in the browser.
- Hub: https://foundry.dap.solutions/hub/

Prefer POST, because GET puts your inputs in the URL, where proxies and logs outside this tool may record them. POST keeps them in the body.

## Payment

Each computed answer from this endpoint costs $0.01, paid in USDC on Base under x402 version 2. No account and no key. The page at https://foundry.dap.solutions/compute-day-count-to-settlement/ and the hub stay free.

A payment edge, a Cloudflare Worker in front of this endpoint, asks for the payment and checks it. The endpoint is unchanged: a paid answer is the endpoint's own answer, byte for byte, with payment headers added.

1. Send the call without payment. The reply is HTTP 402 with a PAYMENT-REQUIRED header, and the same object as the body: base64 JSON in the header, plain JSON in the body. It names the price in USDC's smallest unit (amount 10000, at six decimals), the network eip155:8453 (Base), the USDC contract and the wallet to pay.
2. Sign a USDC transferWithAuthorization (EIP-3009) for that amount to that wallet, and send it, as base64 JSON, in a PAYMENT-SIGNATURE header on the same call.
3. The payment is checked, the endpoint answers, the payment settles, and the answer comes back with a PAYMENT-RESPONSE header that holds the settlement transaction. An answer is released only after its payment settles.

A refusal (HTTP 422) is not charged, and the same signed payment then works on the corrected call while it is still valid. A PAYMENT-SIGNATURE header that cannot be decoded gets 400. A payment that is too old, for another amount or network, already used, or unfunded gets 402 again, with the reason in the error field; the endpoint does not run. If the payment service cannot be reached to check the payment, the reply is 503 and the endpoint does not run. If it cannot confirm settlement after the endpoint has answered, the reply is 503 and the answer is withheld: send the same call with the same signed payment, which can be charged at most once, and do not sign a new one until that retry answers.

Paying is public: the paying wallet, the amount and the time are recorded on Base and seen by the payment facilitator, Coinbase. Your inputs go to this endpoint only, are not sent to the facilitator, and are not stored.

## Parameters

All seven are required and each is given once. A name not listed here is refused. On a GET each value is a string, decoded the way a URL query is: %2F reads as / and + as a space, and inputs echoes the decoded value. A name given more than once, by GET or in a JSON body, is refused, and inputs echoes it as an array of its values in the order given.

| name | type | allowed values |
| --- | --- | --- |
| accrual_start | string | a real calendar date written YYYY-MM-DD, on or before the settlement date, in any year: it is never refused for coverage, because no day count convention reads the calendar |
| trade_date | string | a business day on the calendar, written YYYY-MM-DD, inside the calendar's coverage |
| settlement_lag | string or integer | "0" to "5", or a JSON integer from 0 to 5: business days from trade to settlement |
| calendar | string | ca-payments-canada |
| convention | string | ACT/360, ACT/365F, 30/360, 30E/360 or ACT/ACT-ISDA, exactly |
| acknowledge | string | no-ruling-v1, exactly |
| freshness_tolerance | string | whole days written P<n>D, n from 0 to 9999, no more than ratified_ceiling |

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["accrual_start", "trade_date", "settlement_lag", "calendar", "convention", "acknowledge", "freshness_tolerance"],
  "properties": {
    "accrual_start": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "a real calendar date, the first day of the accrual period, counted" },
    "trade_date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "a business day on the calendar" },
    "settlement_lag": { "oneOf": [{ "type": "string", "pattern": "^[0-5]$" }, { "type": "integer", "minimum": 0, "maximum": 5 }] },
    "calendar": { "type": "string", "enum": ["ca-payments-canada"] },
    "convention": { "type": "string", "enum": ["ACT/360", "ACT/365F", "30/360", "30E/360", "ACT/ACT-ISDA"] },
    "acknowledge": { "type": "string", "enum": ["no-ruling-v1"] },
    "freshness_tolerance": { "type": "string", "pattern": "^P(0|[1-9][0-9]{0,3})D$", "description": "at most the ratified ceiling" }
  }
}
```

Example POST body:

```json
{"accrual_start":"2026-07-01","trade_date":"2026-10-09","settlement_lag":"2","calendar":"ca-payments-canada","convention":"ACT/365F","acknowledge":"no-ruling-v1","freshness_tolerance":"P90D"}
```

The same call as a GET: `?accrual_start=2026-07-01&trade_date=2026-10-09&settlement_lag=2&calendar=ca-payments-canada&convention=ACT%2F365F&acknowledge=no-ruling-v1&freshness_tolerance=P90D`

Both examples declare P90D, the ceiling, and work until 2026-12-23. The calendar was collected at 2026-09-23T04:51:02Z, so from 2026-12-23T04:51:02Z it is 91 days old and a call declaring P90D is refused as INPUT_STALE, until the calendar is collected again and a person Clears the new artifact. freshness_tolerance must be at least the calendar's age in days (calendar_age_days) and at most ratified_ceiling.

## Envelope

Every answer, computed or refused, is one JSON object with these fields in this order.

- tool: "compute-day-count-to-settlement".
- artifact_hash: "sha256:<hex>", the hash of the artifact that answered.
- status: "computed" or "refused".
- refusal: { code, message } when refused, else null.
- result: { settlement_date, days, fraction: { numerator, denominator, decimal } } when computed, else null. numerator and denominator are JSON integers in lowest terms, and zero is 0/1. decimal is a string with exactly 10 places, rounded half to even.
- inputs: the inputs as received; {} when the request could not be read, was too large or nested too deep. A repeated name holds an array of its values. Keys are in JavaScript object key order, as JSON.parse gives them: names that are whole numbers from 0 to 4294967294, written without leading zeros, come first in ascending order, then the rest in the order given. This order is kept as is.
- provenance: { calendar_id, calendar_source_url, calendar_collected_at, calendar_coverage, calendar_age_days }, always the bundled calendar. calendar_age_days is whole days from calendar_collected_at to the clock this tool read, rounded down, or null when that clock reads earlier than calendar_collected_at. The endpoint reads the server's clock. The page and the hub read the device's clock.
- ratified_ceiling: the ceiling a person bound into this tool, written P<n>D, or null when none is bound.
- declared_tolerance: freshness_tolerance as received, or null when it was not sent.
- acknowledgment: "no-ruling-v1" when acknowledge was exactly that, else null.
- disclaimer: "We attest what we collected and when we last looked. We do not issue rulings."
- attestation_badge: "operator-local".
- retention: "none".

## HTTP status

- 200: computed, and paid.
- 422: refused, with any refusal code. Not charged.
- 402: no payment, or a payment that cannot be used. The PAYMENT-REQUIRED header and the body give the price. A HEAD gets 402 with the header and no body, and the endpoint does not run.
- 400: a PAYMENT-SIGNATURE header that cannot be decoded.
- 503: the payment edge is not set up, or the payment service could not check the payment (nothing was computed or charged), or could not confirm settlement (the answer is withheld; send the same call with the same signed payment, which is charged at most once).
- 204: OPTIONS, no body, answered by the payment edge, which allows the headers content-type and payment-signature.
- 405: any other method, answered by the payment edge with an Allow: GET, POST, HEAD, OPTIONS header.

The 402, 400, 503, 204 and 405 answers come from the payment edge, not the envelope, and carry access-control-allow-origin *. The 402, 400, 204 and 405 answers, and a 503 from the payment service, also carry access-control-expose-headers PAYMENT-REQUIRED, PAYMENT-RESPONSE. Every response the endpoint itself gives carries access-control-allow-origin *, access-control-allow-methods GET, HEAD, POST, OPTIONS, access-control-allow-headers content-type and access-control-max-age 86400. Every envelope response also carries content-type application/json; charset=utf-8, cache-control no-store and x-content-type-options nosniff. A GET whose URL is longer than Cloudflare's URL length limit never reaches the endpoint: Cloudflare answers it itself, with none of these headers and no envelope (see Cloudflare's URL length limit under Text tool endpoints). Every GET within this endpoint's own limit of 4,096 bytes reaches it.

## Refusal codes

| code | trigger |
| --- | --- |
| INPUT_INVALID | the request cannot be read as named inputs, is over 4,096 bytes, nests values more than 8 levels deep, holds a number that is not finite, names an unknown parameter, repeats one, lacks calendar, accrual_start, trade_date, settlement_lag or convention, names another calendar, has a value in the wrong form, has a trade_date that is a weekend or a closure day, or has an accrual_start after the settlement date |
| ACK_MISSING | acknowledge is not exactly no-ruling-v1 |
| TOLERANCE_MISSING | freshness_tolerance is absent, null or empty |
| TOLERANCE_INVALID | freshness_tolerance is not whole days written P<n>D |
| TOLERANCE_OVER_CEILING | no ceiling is bound yet, or the declared days are more than the ceiling's |
| CLOCK_BEHIND | the clock this tool read is earlier than calendar_collected_at, even by 1 millisecond, so calendar_age_days is null |
| INPUT_STALE | calendar_age_days is more than the declared days |
| OUT_OF_COVERAGE | trade_date is in a year the calendar does not cover, or counting the lag runs past its last covered day. accrual_start is never refused for coverage |

## Order of checks

The first check that fails is the answer. Missing means absent, null or the empty string.

1. The request is read, and refused as INPUT_INVALID when, in this order, it is over 4,096 bytes; on a POST, is not valid UTF-8; is empty or only white space; nests values more than 8 levels deep (counted over the text before it is parsed, the top level object being level 1); is not a JSON object; or holds a number that is not finite, such as 1e400.
2. acknowledge is exactly no-ruling-v1, or ACK_MISSING.
3. freshness_tolerance is present, or TOLERANCE_MISSING.
4. freshness_tolerance is written P<n>D, or TOLERANCE_INVALID.
5. A ceiling is bound and the tolerance is within it, or TOLERANCE_OVER_CEILING.
6. Every parameter name is known (the first unknown name is taken in that same key order), no name is repeated (by GET or in a JSON body), calendar is present and is ca-payments-canada, or INPUT_INVALID.
7. The clock this tool read is not earlier than calendar_collected_at, or CLOCK_BEHIND.
8. calendar_age_days is no more than the tolerance, or INPUT_STALE.
9. accrual_start, trade_date, settlement_lag and convention, in that order, each present and in form, or INPUT_INVALID.
10. trade_date falls inside the calendar's coverage, or OUT_OF_COVERAGE. accrual_start is not checked for coverage: no day count convention reads the calendar.
11. trade_date is a business day, or INPUT_INVALID.
12. Settlement: from trade_date, step forward one calendar day until a business day, settlement_lag times. Leaving coverage is OUT_OF_COVERAGE. A lag of 0 settles on trade_date.
13. accrual_start is on or before settlement, or INPUT_INVALID.
14. Compute.

A business day is not a Saturday, not a Sunday and not a closure date on the calendar.

## Conventions

ISDA 2006 Definitions, Section 4.16. The period includes accrual_start and excludes the settlement date. A = the actual days between them.

- ACT/360: days = A, fraction = A / 360.
- ACT/365F: days = A, fraction = A / 365.
- ACT/ACT-ISDA: days = A, fraction = the sum over each calendar year the period touches of (days of the period in that year) / (366 in a leap year, else 365).
- 30/360: D1 of 31 becomes 30. D2 of 31 becomes 30 when D1 is then over 29. days = 360 x (Y2 - Y1) + 30 x (M2 - M1) + (D2 - D1), fraction = days / 360.
- 30E/360: the same, except D2 of 31 always becomes 30.

## Calendar

- calendar_id: ca-payments-canada, the Payments Canada system closure schedule.
- Source: https://www.payments.ca/system-closure-schedule
- Collected at: 2026-09-23T04:51:02Z
- Coverage: 2026 to 2026. The source publishes the current year only.
- Business days: Monday to Friday, except the 10 closure days listed for 2026.

The calendar is bound into the artifact. Refreshing it is a new artifact and a new Clear by a person.

## Record

- retention: none. The tool keeps no copy of any request or answer. Each response is the caller's own copy of the record.
- attestation_badge: operator-local. The record is written by the operator. It is not independently signed.
- A new tool publishes only after a person authorizes it.

# Text tool endpoints

Three text tools answer JSON this way: convert-unix-timestamp-to-date, decode-base64-to-text and diff-two-line-lists. Each endpoint hands one text to its tool's core, the same src/core.ts the tool's page runs, so the endpoint and the page answer one text with one core and one artifact hash. None reads a clock or calendar, and none takes acknowledge or freshness_tolerance. This section gives the envelope, the headers, Cloudflare's URL length limit and what white space means. Each tool's own section after it gives its URL, its limits, its parameter, its result, its refusal codes and its order of checks.

## Envelope

Every answer the endpoint gives, computed or refused, is one JSON object with these fields in this order, written as JSON indented by two spaces and ending in a line feed. A request Cloudflare answers itself gets no envelope: see Cloudflare's URL length limit, below.

- tool: the tool's slug.
- artifact_hash: "sha256:<hex>", the hash of the artifact that answered.
- status: "computed" or "refused".
- refusal: { code, message } when refused, else null. For a code from the tool's core, message is what the tool's page shows for that code: the code's message, then, when the core gives a detail, one space and the detail in parentheses. For INPUT_INVALID, message is one of the sentences under INPUT_INVALID messages below, word for word.
- result: { output, summary } when computed, else null. output is what the tool's page puts in its Output box, and summary is the line the page shows beside Copy and Download.
- inputs: the inputs as received, text in full; {} when the request could not be read, was too large, nested values too deep or held too many values, or used a method other than GET, HEAD or POST. A repeated name holds an array of its values. Keys are in JavaScript object key order, as JSON.parse gives them: names that are whole numbers from 0 to 4294967294, written without leading zeros, come first in ascending order, then the rest in the order given. This order is kept as is.
- disclaimer: "DAP Foundry · dap.solutions · no warranty · processed in memory · inputs not stored", the site footer, word for word.
- attestation_badge: "operator-local".
- retention: "none".

## INPUT_INVALID messages

The first check that fails, in the tool's order of checks, picks the message. Each message is written here exactly as sent. EXAMPLE stands for the tool's example, written as it appears in the message, with each \n as the two characters backslash and n:

| tool | EXAMPLE |
| --- | --- |
| convert-unix-timestamp-to-date | {"text":"1700000000\n1700000000000\n-86400"} |
| decode-base64-to-text | {"text":"aGVsbG8="} |
| diff-two-line-lists | {"text":"---A---\na\nb\n---B---\nb\nc\n"} |

| when | message |
| --- | --- |
| the request is over 1,052,672 bytes, by its content-length header, as its body is read, or as a query that reaches the endpoint | The request is larger than 1,052,672 bytes. Send one JSON object with one name, text. Nothing was computed. |
| a POST body nests values more than 8 levels deep | The request nests values more than 8 levels deep. Send one JSON object with one name, text, whose value is a string. Nothing was computed. |
| a POST body holds more than 64 values, or a query more than 64 pairs | The request holds more than 64 values. Send one JSON object with one name, text, whose value is a string. Nothing was computed. |
| a POST body cannot be read to its end, is not valid UTF-8, is empty or only white space, is not one JSON object or holds a number that is not finite; or the method is not GET, HEAD, POST or OPTIONS | The request could not be read as a set of named inputs. Send a JSON object such as EXAMPLE, or text as a query parameter on a GET. Nothing was computed. |
| a name other than text is given | NAME is not an input this tool reads. It reads one: text. Nothing was computed. |
| text is given more than once | text was given more than once. Give it once. Nothing was computed. |
| text is absent or null | No text was given. Send a JSON object such as EXAMPLE, or text as a query parameter on a GET. Nothing was computed. |
| text is not a string | text must be a string. Send a JSON object such as EXAMPLE, or text as a query parameter on a GET. Nothing was computed. |

NAME is the first other name, in the key order inputs shows, written as a JSON string in double quotes. A name longer than 40 characters, counted as JavaScript counts a string's length, is cut to its first 40 and an ellipsis (…) is added inside the quotes.

## HTTP status and headers

- 200: computed.
- 422: refused, with any refusal code.
- 204: OPTIONS, no body.
- Any other method gets 422 with INPUT_INVALID, the unreadable message, and an Allow: GET, HEAD, POST, OPTIONS header.

Every response the endpoint gives carries access-control-allow-origin *, access-control-allow-methods GET, HEAD, POST, OPTIONS, access-control-allow-headers content-type and access-control-max-age 86400. Every envelope response also carries content-type application/json; charset=utf-8, cache-control no-store and x-content-type-options nosniff. A request Cloudflare answers itself carries none of these headers.

## Cloudflare's URL length limit

Each endpoint is a Cloudflare Pages Function, and every request passes through Cloudflare before it reaches the function. A GET or HEAD whose URL is longer than Cloudflare's URL length limit never reaches the endpoint: Cloudflare answers it itself. That answer is not the envelope, names no refusal code and carries none of the headers above, access-control-allow-origin among them, so a page on another origin cannot read it in a browser. Cloudflare's URL length limit is much shorter than 1,052,672 bytes, so the endpoint's own request limit, and the INPUT_INVALID it gives, apply to a query only when the query reaches it. A POST body is not part of the URL: the endpoint reads it up to its own limit of 1,052,672 bytes. Send a long text by POST.

## White space

White space means the characters JavaScript counts as white space, which the core reads: tab, line feed, vertical tab, form feed, carriage return, space, the no-break space U+00A0, U+1680, U+2000 to U+200A, U+2028, U+2029, U+202F, U+205F, U+3000 and U+FEFF. The next line character U+0085 is white space in Unicode but not in JavaScript, so the core does not trim it. U+FEFF is not white space in Unicode but is in JavaScript, so the core trims it like a space. The tool's section says what that means for its text.

## Record

- retention: none. An endpoint receives only what a caller sends it and keeps no copy of any request or answer; inputs are not stored. Each response is the caller's own copy of the record.
- attestation_badge: operator-local. The record is written by the operator. It is not independently signed.

# convert-unix-timestamp-to-date

Give one or more Unix timestamps, one per line. Get a CSV with each one as a UTC date in ISO 8601.

## Endpoint

- URL: https://foundry.dap.solutions/api/convert-unix-timestamp-to-date
- Surface: /api/convert-unix-timestamp-to-date, a stateless Cloudflare Pages Function.
- Methods: GET with the query parameter text. POST with a JSON object as the body, any content-type. HEAD returns the status and headers of the GET, with no body. OPTIONS returns 204 for a CORS preflight. Any other method is refused.
- Size: the endpoint takes a request of at most 1,052,672 bytes of UTF-8, counted over the raw bytes of a POST body, or over the query string (without the leading ?) of a GET or HEAD that reaches it. A GET or HEAD whose URL is longer than Cloudflare's URL length limit, which is much shorter than 1,052,672 bytes, is answered by Cloudflare and not by the endpoint, with no envelope and no CORS header (see Cloudflare's URL length limit, under Text tool endpoints). A content-length header is optional. A POST whose content-length is over 1,052,672 is refused without reading the body. Otherwise the body is read in chunks and the read stops as soon as it passes 1,052,672 bytes. A body that is not valid UTF-8 is unreadable, and a byte order mark at its very start is dropped. 1,052,672 is the text limit of 1,048,576 bytes plus 4,096, but the two limits count different things: the request is counted as sent, every escape included, and text as the core reads it, once the JSON or the query is decoded. So a text near its limit that needs many escapes in JSON (line breaks written \n, tabs, quotes, backslashes, control characters) can be too large to send, and is then refused as INPUT_INVALID, with the message that the request is larger than 1,052,672 bytes, before anything is parsed. Send such a text raw where JSON allows it, escaping only a quote, a backslash and the characters below U+0020, or split it. For this tool a line feed is written \n in JSON, two bytes for one, and a CRLF \r\n, four bytes for two. White space around a timestamp, which the core trims, can be left out without changing a row, and the lines can be split between requests.
- Text: at most 1,048,576 bytes of UTF-8, counted over text as the core reads it, and 10,000 lines.
- Depth: on a POST, values may nest at most 8 levels deep, counting the top level object as level 1. Every { and [ outside a string opens a level, counted over the body before it is parsed.
- Values: at most 64. On a POST every {, [ and , outside a string counts one, over the body before it is parsed, so {"text":"..."} holds 1. On a GET each name and value pair counts one, counted as one more than the number of & in the query before it is decoded.
- Page: https://foundry.dap.solutions/convert-unix-timestamp-to-date/ runs the same core in the browser.

Prefer POST, because GET puts your inputs in the URL, where proxies and logs outside this tool may record them. POST keeps them in the body. A long text does not fit in a URL, and in a query each byte written as a percent escape, such as %0A, counts three bytes toward the request limit, so send it by POST.

## Parameters

text is required and is given once. A name other than text is refused. On a GET the value is a string, decoded the way a URL query is: %0A reads as a line feed, %2B as + and a bare + as a space, and inputs echoes the decoded value. Write each line break as %0A. A name given more than once, by GET or in a JSON body, is refused, and inputs echoes it as an array of its values in the order given.

| name | type | allowed values |
| --- | --- | --- |
| text | string | one or more Unix timestamps, one per line: whole seconds or milliseconds, digits with an optional leading minus, inside the years 0001 to 9999; at most 1,048,576 bytes of UTF-8 and 10,000 lines, where a line break at the very end is not a line |

Each line is trimmed: white space, as under White space in Text tool endpoints, is removed from its start and end, U+FEFF included. The next line character U+0085 is not trimmed, so a line that holds it is refused as NOT_A_TIMESTAMP. U+2028 and U+2029 are white space here and do not end a line: only a line feed does.

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["text"],
  "properties": {
    "text": { "type": "string", "description": "One or more Unix timestamps, one per line: whole seconds or milliseconds, digits with an optional leading minus, inside the years 0001 to 9999. At most 1,048,576 bytes of UTF-8 and 10,000 lines; a line break at the very end is not a line." }
  }
}
```

Example POST body:

```json
{"text":"1700000000\n1700000000000\n-86400"}
```

The same call as a GET: `?text=1700000000%0A1700000000000%0A-86400`

Both send the valid example the page shows with an error, and both compute the summary 3 timestamps · 2 seconds, 1 milliseconds.

## Envelope

The fields, in order, are those under Text tool endpoints, and tool is "convert-unix-timestamp-to-date". When computed, result holds:

- output: CSV with the header input,unit,iso_utc, then one row per line that is not blank, in the order given: the line as trimmed, seconds or milliseconds, and the date in UTC written YYYY-MM-DDTHH:MM:SSZ. A magnitude at or above 100000000000, ignoring the minus sign, is milliseconds, and below it is seconds. Milliseconds are rounded down to the whole second, before 1970 too. Every line of the CSV, the header included, ends in CRLF, the last one too.
- summary: the number of timestamps, then how many were seconds and how many milliseconds, such as 3 timestamps · 2 seconds, 1 milliseconds.

## HTTP status and headers

200 when computed, 422 when refused, 204 for OPTIONS, and 422 with INPUT_INVALID and an Allow: GET, HEAD, POST, OPTIONS header for any other method. The headers are those under Text tool endpoints.

## Refusal codes

| code | trigger |
| --- | --- |
| INPUT_INVALID | the request is over 1,052,672 bytes as sent, escapes included, which a text within its own limit can be when it needs many escapes; a POST body cannot be read to its end, is not valid UTF-8, is empty or only white space, nests values more than 8 levels deep, holds more than 64 values, is not one JSON object, or holds a number that is not finite, such as 1e400; a GET query holds more than 64 pairs; the method is not GET, HEAD, POST or OPTIONS; a name other than text is given; text is given more than once; text is absent or null; or text is not a string |
| EMPTY_INPUT | text is empty or only white space, or no line holds a timestamp |
| MAX_BYTES | text is over 1,048,576 bytes of UTF-8 |
| MAX_LINES | text has more than 10,000 lines; the detail is the line count, written with a comma between each group of three digits, such as (10,001 lines) |
| NOT_A_TIMESTAMP | a line, trimmed, is not digits with an optional leading minus, or its date falls outside the years 0001 to 9999; the detail names the first such line by its number and shows it as sent, not trimmed, any carriage return or other white space in it included, cut to 40 characters and an ellipsis when longer, such as (line 2: nope) |

EMPTY_INPUT and MAX_BYTES carry no detail.

## Order of checks

The first check that fails is the answer. A GET or HEAD whose URL is longer than Cloudflare's URL length limit reaches none of these checks: Cloudflare answers it.

1. On a POST the request is read, and refused as INPUT_INVALID when, in this order: its content-length header is a whole number over 1,052,672; its body passes 1,052,672 bytes as it is read; the body cannot be read to its end or is not valid UTF-8; it is empty or only white space; it nests values more than 8 levels deep; it holds more than 64 values; it is not one JSON object; or it holds a number that is not finite. On a GET or a HEAD that reaches the endpoint the query is refused as INPUT_INVALID when it is over 1,052,672 bytes, then when it holds more than 64 pairs. Any other method is refused as INPUT_INVALID.
2. Every name is text, or INPUT_INVALID. The first other name is taken in the key order inputs shows.
3. text is given once, or INPUT_INVALID.
4. text is present and not null, or INPUT_INVALID.
5. text is a string, or INPUT_INVALID. From here the checks are the core's, as on the page.
6. text is not empty or only white space, or EMPTY_INPUT.
7. text is at most 1,048,576 bytes of UTF-8, or MAX_BYTES.
8. text has at most 10,000 lines, or MAX_LINES. A line ends at a line feed, with or without a carriage return before it. A line break at the very end does not start another line. Blank lines count.
9. Each line in order, trimmed, is blank and gives no row, or is digits with an optional leading minus whose date falls inside the years 0001 to 9999, or NOT_A_TIMESTAMP.
10. At least one line gave a row, or EMPTY_INPUT.
11. Compute.

## Page and endpoint

The endpoint reads text exactly as sent. The page's text box turns every CRLF and every lone carriage return into a line feed before the core reads it. So at the endpoint a lone carriage return does not end a line. At the start or end of a line it is trimmed like other white space, so when the page computes, the rows are the ones the page gives, but the line count, and the line number in a NOT_A_TIMESTAMP detail, can be lower than on the page. The lines those carriage returns add on the page can take the page's line count over 10,000, so the page can refuse as MAX_LINES a text the endpoint computes: 5,001 timestamps, each on a line that starts with a lone carriage return, are 5,001 lines at the endpoint, which computes them, and 10,002 lines on the page, which refuses them. Between two timestamps a lone carriage return leaves them on one line, refused as NOT_A_TIMESTAMP, where the page reads two lines. The carriage returns of CRLF line breaks count toward the 1,048,576 byte limit at the endpoint, and not on the page. The line count in the MAX_LINES detail is written with a comma between each group of three digits, such as (10,001 lines), and no locale is read, so the page and the endpoint write it the same way.

# decode-base64-to-text

Give base64. Get the UTF-8 text it decodes to.

## Endpoint

- URL: https://foundry.dap.solutions/api/decode-base64-to-text
- Surface: /api/decode-base64-to-text, a stateless Cloudflare Pages Function.
- Methods: GET with the query parameter text. POST with a JSON object as the body, any content-type. HEAD returns the status and headers of the GET, with no body. OPTIONS returns 204 for a CORS preflight. Any other method is refused.
- Size: the endpoint takes a request of at most 1,052,672 bytes of UTF-8, counted over the raw bytes of a POST body, or over the query string (without the leading ?) of a GET or HEAD that reaches it. A GET or HEAD whose URL is longer than Cloudflare's URL length limit, which is much shorter than 1,052,672 bytes, is answered by Cloudflare and not by the endpoint, with no envelope and no CORS header (see Cloudflare's URL length limit, under Text tool endpoints). A content-length header is optional. A POST whose content-length is over 1,052,672 is refused without reading the body. Otherwise the body is read in chunks and the read stops as soon as it passes 1,052,672 bytes. A body that is not valid UTF-8 is unreadable, and a byte order mark at its very start is dropped. 1,052,672 is the text limit of 1,048,576 bytes plus 4,096, but the two limits count different things: the request is counted as sent, every escape included, and text as the core reads it, once the JSON or the query is decoded. So a text near its limit that needs many escapes in JSON (line breaks written \n, tabs, quotes, backslashes, control characters) can be too large to send, and is then refused as INPUT_INVALID, with the message that the request is larger than 1,052,672 bytes, before anything is parsed. For this tool a line feed is written \n in JSON, two bytes for one, and a CRLF \r\n, four bytes for two, so base64 broken into lines (MIME, for one, breaks it every 76 characters) can be too large to send when the text is near its limit. White space, which the core removes, can be left out without changing the output, so send such a text with its line breaks left out.
- Text: at most 1,048,576 bytes of UTF-8, counted over text as the core reads it, white space included. There is no line limit.
- Depth: on a POST, values may nest at most 8 levels deep, counting the top level object as level 1. Every { and [ outside a string opens a level, counted over the body before it is parsed.
- Values: at most 64. On a POST every {, [ and , outside a string counts one, over the body before it is parsed, so {"text":"..."} holds 1. On a GET each name and value pair counts one, counted as one more than the number of & in the query before it is decoded.
- Page: https://foundry.dap.solutions/decode-base64-to-text/ runs the same core in the browser.

Prefer POST, because GET puts your inputs in the URL, where proxies and logs outside this tool may record them. POST keeps them in the body. A long text does not fit in a URL, and in a query each byte written as a percent escape, such as the %2B each + in base64 needs, counts three bytes toward the request limit, so send it by POST.

## Parameters

text is required and is given once. A name other than text is refused. On a GET the value is a string, decoded the way a URL query is: %2B reads as +, %2F as /, %3D as =, %0A as a line feed and a bare + as a space, and inputs echoes the decoded value. + is part of the base64 alphabet, so on a GET send each + in the base64 as %2B. A bare + reads as a space, which the core removes as white space, so the base64 decodes to other bytes or is refused: ?text=Pz8%2B decodes to ??>, while ?text=Pz8+ reads as Pz8 and a space and decodes to ??. / and = can be sent as they are or as %2F and %3D: the first = in a pair ends the name, and any = after it is part of the value. A name given more than once, by GET or in a JSON body, is refused, and inputs echoes it as an array of its values in the order given.

| name | type | allowed values |
| --- | --- | --- |
| text | string | base64 in the standard alphabet that decodes to UTF-8 text: the letters A to Z and a to z, the digits 0 to 9, + and /, then one or two = of padding at the end, which may be left out but, when given, must make the length a multiple of four; a length that does not leave one character over a multiple of four; white space anywhere, which is ignored and not counted in the length; at most 1,048,576 bytes of UTF-8, white space included |

White space, as under White space in Text tool endpoints, is removed from anywhere in text before the base64 is read, U+FEFF included. The next line character U+0085 is not white space here, so a text that holds it is refused as INVALID_BASE64. The URL safe letters - and _ are not in the alphabet, so a text that holds them is refused as INVALID_BASE64: change each - to + and each _ to / before sending it.

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["text"],
  "properties": {
    "text": { "type": "string", "description": "Base64 that decodes to UTF-8 text: the letters A to Z and a to z, the digits 0 to 9, + and /, then one or two = of padding at the end, which may be left out but, when given, must make the length a multiple of four. A length that leaves one character over a multiple of four is refused. White space anywhere is ignored and not counted in the length. At most 1,048,576 bytes of UTF-8, white space included." }
  }
}
```

Example POST body:

```json
{"text":"aGVsbG8="}
```

The same call as a GET: `?text=aGVsbG8=`

Both send the valid example the page shows with an error, and both compute the output hello and the summary 5 characters.

## Envelope

The fields, in order, are those under Text tool endpoints, and tool is "decode-base64-to-text". When computed, result holds:

- output: the text the base64 decodes to, read as UTF-8, whole. A byte order mark (the bytes EF BB BF) at its very start is dropped, and one anywhere else is kept, so output is empty when the base64 decodes to a byte order mark alone, such as 77u/, and the result is still computed. Bits left over after the last whole byte are ignored, so aGk= and aGl= both decode to hi. Characters below U+0020, such as a NUL or a line feed, are kept, and the envelope writes them as JSON escapes, such as \u0000 and \n.
- summary: the number of characters in output, counted as JavaScript counts a string's length, then character when the count is 1 and characters otherwise, such as 5 characters, 1 character or 0 characters. The count is written in digits with no comma, such as 120000 characters. A character outside the Basic Multilingual Plane, such as the emoji U+1F600, counts as two: aGkg8J+YgA== decodes to hi, a space and U+1F600, and gives 5 characters.

## HTTP status and headers

200 when computed, 422 when refused, 204 for OPTIONS, and 422 with INPUT_INVALID and an Allow: GET, HEAD, POST, OPTIONS header for any other method. The headers are those under Text tool endpoints.

## Refusal codes

| code | trigger |
| --- | --- |
| INPUT_INVALID | the request is over 1,052,672 bytes as sent, escapes included, which a text within its own limit can be when it needs many escapes; a POST body cannot be read to its end, is not valid UTF-8, is empty or only white space, nests values more than 8 levels deep, holds more than 64 values, is not one JSON object, or holds a number that is not finite, such as 1e400; a GET query holds more than 64 pairs; the method is not GET, HEAD, POST or OPTIONS; a name other than text is given; text is given more than once; text is absent or null; or text is not a string |
| EMPTY_INPUT | text is empty or only white space |
| MAX_BYTES | text is over 1,048,576 bytes of UTF-8, white space included |
| INVALID_BASE64 | once white space is removed, text holds a character other than the letters A to Z and a to z, the digits 0 to 9, + and /, or an = anywhere but as one or two at the very end; its length leaves one character over a multiple of four; it ends in = and its length is not a multiple of four; or the bytes it decodes to are not UTF-8 text |

No refusal from this core carries a detail, so for EMPTY_INPUT, MAX_BYTES and INVALID_BASE64 the message is the code's message in errors.json, word for word. EXAMPLE, in the INPUT_INVALID messages under Text tool endpoints, is {"text":"aGVsbG8="} for this tool.

## Order of checks

The first check that fails is the answer. A GET or HEAD whose URL is longer than Cloudflare's URL length limit reaches none of these checks: Cloudflare answers it.

1. On a POST the request is read, and refused as INPUT_INVALID when, in this order: its content-length header is a whole number over 1,052,672; its body passes 1,052,672 bytes as it is read; the body cannot be read to its end or is not valid UTF-8; it is empty or only white space; it nests values more than 8 levels deep; it holds more than 64 values; it is not one JSON object; or it holds a number that is not finite. On a GET or a HEAD that reaches the endpoint the query is refused as INPUT_INVALID when it is over 1,052,672 bytes, then when it holds more than 64 pairs. Any other method is refused as INPUT_INVALID.
2. Every name is text, or INPUT_INVALID. The first other name is taken in the key order inputs shows.
3. text is given once, or INPUT_INVALID.
4. text is present and not null, or INPUT_INVALID.
5. text is a string, or INPUT_INVALID. From here the checks are the core's, as on the page.
6. text is not empty or only white space, or EMPTY_INPUT.
7. text is at most 1,048,576 bytes of UTF-8, white space included, or MAX_BYTES.
8. Every white space character is removed from text. What is left is the letters A to Z and a to z, the digits 0 to 9, + and /, then at most two =, and its length does not leave one character over a multiple of four, or INVALID_BASE64.
9. What is left is decoded as base64. If it ends in =, its length is a multiple of four, or INVALID_BASE64. Bits left over after the last whole byte are ignored.
10. The bytes are UTF-8 text, read strictly, or INVALID_BASE64. One byte that is not part of UTF-8 text refuses the whole text, and no part of it is given.
11. Compute. A byte order mark at the very start of the text is dropped.

## Page and endpoint

The endpoint reads text exactly as sent. The page's text box turns every CRLF and every lone carriage return into a line feed before the core reads it. Carriage returns and line feeds are white space, which the core removes before it reads the base64, so the output is the same either way. But the carriage returns of CRLF line breaks count toward the 1,048,576 byte limit at the endpoint, and not on the page, so the endpoint can refuse as MAX_BYTES a text the page computes: base64 in lines that end in CRLF, within the limit on the page, can be over it at the endpoint. Leave out the line breaks, which change nothing in the output. output is the decoded text exactly, the text the page's Copy and Download give.

# diff-two-line-lists

Give two lists of lines, list A after a line ---A--- and list B after a line ---B---. Get a CSV that marks each distinct line only_a, only_b or both.

## Endpoint

- URL: https://foundry.dap.solutions/api/diff-two-line-lists
- Surface: /api/diff-two-line-lists, a stateless Cloudflare Pages Function.
- Methods: GET with the query parameter text. POST with a JSON object as the body, any content-type. HEAD returns the status and headers of the GET, with no body. OPTIONS returns 204 for a CORS preflight. Any other method is refused.
- Size: the endpoint takes a request of at most 1,052,672 bytes of UTF-8, counted over the raw bytes of a POST body, or over the query string (without the leading ?) of a GET or HEAD that reaches it. A GET or HEAD whose URL is longer than Cloudflare's URL length limit, which is much shorter than 1,052,672 bytes, is answered by Cloudflare and not by the endpoint, with no envelope and no CORS header (see Cloudflare's URL length limit, under Text tool endpoints). A content-length header is optional. A POST whose content-length is over 1,052,672 is refused without reading the body. Otherwise the body is read in chunks and the read stops as soon as it passes 1,052,672 bytes. A body that is not valid UTF-8 is unreadable, and a byte order mark at its very start is dropped. 1,052,672 is the text limit of 1,048,576 bytes plus 4,096, but the two limits count different things: the request is counted as sent, every escape included, and text as the core reads it, once the JSON or the query is decoded. So a text near its limit that needs many escapes in JSON (line breaks written \n, tabs, quotes, backslashes, control characters) can be too large to send, and is then refused as INPUT_INVALID, with the message that the request is larger than 1,052,672 bytes, before anything is parsed. Send such a text raw where JSON allows it, escaping only a quote, a backslash and the characters below U+0020, or make it shorter. For this tool a line feed is written \n in JSON, two bytes for one, and a CRLF \r\n, four bytes for two. When the text computes, the lines before the first ---A--- are ignored, so they can be left out without changing a row. The two lists must be sent in one request: each request compares only the lists it holds.
- Text: at most 1,048,576 bytes of UTF-8, counted over text as the core reads it, and 50,000 lines.
- Depth: on a POST, values may nest at most 8 levels deep, counting the top level object as level 1. Every { and [ outside a string opens a level, counted over the body before it is parsed.
- Values: at most 64. On a POST every {, [ and , outside a string counts one, over the body before it is parsed, so {"text":"..."} holds 1. On a GET each name and value pair counts one, counted as one more than the number of & in the query before it is decoded.
- Page: https://foundry.dap.solutions/diff-two-line-lists/ runs the same core in the browser.

Prefer POST, because GET puts your inputs in the URL, where proxies and logs outside this tool may record them. POST keeps them in the body. A long text does not fit in a URL, and in a query each byte written as a percent escape, such as %0A, counts three bytes toward the request limit, so send it by POST.

## Parameters

text is required and is given once. A name other than text is refused. On a GET the value is a string, decoded the way a URL query is: %0A reads as a line feed, %0D as a carriage return, %2B as +, and %20 and a bare + as a space, and inputs echoes the decoded value. Write each line break as %0A, and each + in a line as %2B: sent bare, +x is compared as a space then x. A name given more than once, by GET or in a JSON body, is refused, and inputs echoes it as an array of its values in the order given.

| name | type | allowed values |
| --- | --- | --- |
| text | string | a line ---A---, then list A, then a line ---B---, then list B, one item per line; each marker exactly ---A--- or ---B---, upper case, alone on its line, with no spaces; either list may be empty; at most 1,048,576 bytes of UTF-8 and 50,000 lines, where a line break at the very end is not a line |

A line ends at a line feed, with or without a carriage return just before it. Only a line feed ends a line: a lone carriage return, a vertical tab, a form feed, U+0085, U+2028 and U+2029 stay part of the line. Nothing is trimmed. Each line is compared exactly as sent, so case, spaces, tabs, a byte order mark and every other character matter. Lines before the first ---A--- are ignored. White space, as under White space in Text tool endpoints, is read only to refuse a text that is empty or only white space.

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["text"],
  "properties": {
    "text": { "type": "string", "description": "Two lists of lines: a line ---A---, then list A, then a line ---B---, then list B, one item per line. At most 1,048,576 bytes of UTF-8 and 50,000 lines; a line break at the very end is not a line." }
  }
}
```

Example POST body:

```json
{"text":"---A---\na\nb\n---B---\nb\nc\n"}
```

The same call as a GET: `?text=---A---%0Aa%0Ab%0A---B---%0Ab%0Ac%0A`

Both send the valid example the page shows with an error, and both compute the summary 3 lines.

## Envelope

The fields, in order, are those under Text tool endpoints, and tool is "diff-two-line-lists". When computed, result holds:

- output: CSV with the header line,side. List A is the lines after the first ---A--- and before the first ---B---, less one blank line just before ---B---. List B is the lines after the first ---B---; a line break at the very end only closes the last line, so it adds no blank line to list B. First comes one row for each distinct line of list A, in the order it first appears there, marked both when list B holds the same line and only_a when it does not. Then comes one row for each distinct line of list B that list A does not hold, in the order it first appears in B, marked only_b. A row is the line as its first cell, a comma, then the side. A blank line gives an empty first cell. Every line of the CSV, the header included, ends in CRLF, the last one too.
- summary: the number of rows, then line or lines, such as 3 lines or 1 line, and 0 lines when both lists are empty.

Lines are matched exactly as sent, before either of these changes to a cell. A cell that starts with =, +, -, @, a tab, a carriage return or a byte order mark gets a leading apostrophe ('), unless the whole cell is a plain number: an optional + or -, then digits with at most one decimal point and at least one digit in all, then an optional exponent, e or E with an optional sign and at least one digit, such as -5, +1.5e3 or -.5. Then a cell that holds a comma, a double quote, a semicolon, a tab or a carriage return is wrapped in double quotes, each double quote inside doubled, so the apostrophe sits inside the quotes: =1,2 becomes "'=1,2". A line that already starts with an apostrophe gets no second one, so =1 and '=1 are two lines that both show as '=1.

## HTTP status and headers

200 when computed, 422 when refused, 204 for OPTIONS, and 422 with INPUT_INVALID and an Allow: GET, HEAD, POST, OPTIONS header for any other method. The headers are those under Text tool endpoints.

## Refusal codes

| code | trigger |
| --- | --- |
| INPUT_INVALID | the request is over 1,052,672 bytes as sent, escapes included, which a text within its own limit can be when it needs many escapes; a POST body cannot be read to its end, is not valid UTF-8, is empty or only white space, nests values more than 8 levels deep, holds more than 64 values, is not one JSON object, or holds a number that is not finite, such as 1e400; a GET query holds more than 64 pairs; the method is not GET, HEAD, POST or OPTIONS; a name other than text is given; text is given more than once; text is absent or null; or text is not a string |
| EMPTY_INPUT | text is empty or only white space |
| MAX_BYTES | text is over 1,048,576 bytes of UTF-8 |
| MAX_LINES | text has more than 50,000 lines; the detail is the line count in digits only, with no grouping and no word, such as (50001) |
| BAD_FORMAT | no line is exactly ---A---, no line is exactly ---B---, or the first ---B--- comes before the first ---A--- |

EMPTY_INPUT, MAX_BYTES and BAD_FORMAT carry no detail. In the INPUT_INVALID messages under Text tool endpoints, EXAMPLE for this tool is {"text":"---A---\na\nb\n---B---\nb\nc\n"}, with each \n as the two characters backslash and n.

## Order of checks

The first check that fails is the answer. A GET or HEAD whose URL is longer than Cloudflare's URL length limit reaches none of these checks: Cloudflare answers it.

1. On a POST the request is read, and refused as INPUT_INVALID when, in this order: its content-length header is a whole number over 1,052,672; its body passes 1,052,672 bytes as it is read; the body cannot be read to its end or is not valid UTF-8; it is empty or only white space; it nests values more than 8 levels deep; it holds more than 64 values; it is not one JSON object; or it holds a number that is not finite. On a GET or a HEAD that reaches the endpoint the query is refused as INPUT_INVALID when it is over 1,052,672 bytes, then when it holds more than 64 pairs. Any other method is refused as INPUT_INVALID.
2. Every name is text, or INPUT_INVALID. The first other name is taken in the key order inputs shows.
3. text is given once, or INPUT_INVALID.
4. text is present and not null, or INPUT_INVALID.
5. text is a string, or INPUT_INVALID. From here the checks are the core's, as on the page.
6. text is not empty or only white space, as under White space in Text tool endpoints, or EMPTY_INPUT.
7. text is at most 1,048,576 bytes of UTF-8, or MAX_BYTES.
8. text has at most 50,000 lines, or MAX_LINES. A line ends at a line feed, with or without a carriage return just before it. A line break at the very end does not start another line. Blank lines, the marker lines and the lines before ---A--- count.
9. Some line is exactly ---A---, some line is exactly ---B---, and the first ---A--- comes before the first ---B---, or BAD_FORMAT. Exactly means the whole line as sent: a line with a space, a lower case letter, a byte order mark or a lone carriage return in it is an ordinary line.
10. Compute. A later ---A--- or ---B--- line is compared as an ordinary line.

A text over both limits of checks 7 and 8 never reaches them at the endpoint: more than 50,000 lines takes at least 50,000 line feeds, which add at least 50,000 bytes as escapes in JSON and 100,000 in a query, so the request is over 1,052,672 bytes and check 1 refuses it, unless Cloudflare answers a GET first.

## Page and endpoint

The endpoint reads text exactly as sent. The page's text box turns every CRLF and every lone carriage return into a line feed before the core reads it. So at the endpoint a lone carriage return does not end a line: it stays part of its line and is compared with it, its cell is quoted, and the cell gets the apostrophe when the carriage return starts the line. A marker followed by a lone carriage return, such as ---A---, a carriage return, then a, is not a marker at the endpoint, so the endpoint can refuse as BAD_FORMAT a text the page computes. Between two items a lone carriage return leaves them one line, compared as one item, where the page reads two. The line count can be lower at the endpoint than on the page, so the page can refuse as MAX_LINES a text the endpoint computes: ---A---, then 25,000 lines that are each a carriage return followed by a, then ---B--- are 25,002 lines at the endpoint, which computes them, and 50,002 lines on the page, which refuses them. When both refuse as MAX_LINES, the count in the detail can be lower at the endpoint. The carriage returns of CRLF line breaks count toward the 1,048,576 byte limit at the endpoint, and not on the page. The line count in the MAX_LINES detail is written in digits only, with no grouping and no word, such as (50001), and no locale is read, so the page and the endpoint write it the same way.

# MCP server

One stateless MCP server answers at https://foundry.dap.solutions/mcp, over Streamable HTTP. Its tools are the four endpoints in this file, each named exactly as its slug: compute-day-count-to-settlement, convert-unix-timestamp-to-date, decode-base64-to-text and diff-two-line-lists. Each tool's input schema is its endpoint's parameters, the JSON schema in that endpoint's section, unchanged.

## How it answers

The server computes nothing. A tools/call is sent to the tool's own endpoint, https://foundry.dap.solutions/api/ and the tool's name, as a POST whose body is the call's arguments as JSON.stringify writes them. The result carries that endpoint's answer:

- structuredContent: the endpoint's envelope, refusals included.
- content: one text block, the exact text the endpoint sent.
- isError: true when the envelope's status is refused, else false.

So an answer through MCP is the endpoint's answer, with the same artifact_hash. The arguments are read as part of the JSON-RPC message before they reach the endpoint: a name given twice keeps its last value, a number too large to be finite reaches the endpoint as null, and arguments left out are sent as {}. White space and escapes the caller wrote in the arguments are not kept, so a request the endpoint would refuse as too large can compute through MCP. A request to a text tool through MCP can be larger than the same arguments sent to its endpoint directly, because of the JSON-RPC message around them; see Size.

| when | answer |
| --- | --- |
| the tool name is not exactly one of the four, or is not a string | JSON-RPC error -32602; no endpoint is called |
| arguments is not a JSON object | JSON-RPC error -32602; no endpoint is called |
| the endpoint cannot be reached, or answers without its envelope | JSON-RPC error -32603 with HTTP 502; nothing was computed |

## Protocol

- POST only, one JSON-RPC message per request, answered as one JSON object with content-type application/json. GET and DELETE get 405. OPTIONS gets 204 for a CORS preflight. A batch is refused with -32600 and HTTP 400. A body that is not valid UTF-8, or not JSON, gets -32700 and HTTP 400. A call to compute-day-count-to-settlement sent as a notification, with no id, gets -32600 and HTTP 400.
- Size: a request of at most 1,060,864 bytes. A larger one gets HTTP 413 and -32600, and nothing is read past the limit.
- Version 2026-07-28: every request carries io.modelcontextprotocol/protocolVersion in params._meta, and the headers MCP-Protocol-Version, Mcp-Method and, on tools/call, Mcp-Name must match the body; a missing or different header gets HTTP 400 and -32020. params._meta must also hold io.modelcontextprotocol/clientCapabilities as an object, or the request gets HTTP 400 and -32602. The methods are server/discover, tools/list and tools/call. A version the server does not speak gets HTTP 400 and -32022, with the versions it speaks. Any other method gets HTTP 404 and -32601.
- Versions 2025-11-25, 2025-06-18 and 2025-03-26: initialize, ping, tools/list and tools/call. initialize answers with the version asked for when it is one of these, and 2025-11-25 otherwise. No session is kept: Mcp-Session-Id is never set and is ignored when sent. A notification gets HTTP 202 and no body.
- No sessions and no storage: the server keeps no request and no answer once it has replied.
- An Origin header, when present, must be one http or https origin, or the request gets HTTP 403.
- tools/list gives the four tools in the order above, the same in every era. Under 2026-07-28 it may be kept for an hour (ttlMs 3600000, cacheScope public).

## Payment

compute-day-count-to-settlement costs $0.01 per computed answer here too, under the x402 MCP transport; the other three tools are free.

1. Call the tool without payment. The result has isError true, and its structuredContent is the price: the same object the endpoint's PAYMENT-REQUIRED header holds.
2. Sign a USDC transferWithAuthorization for that amount to that wallet, and send the payment object in params._meta["x402/payment"] on the same call.
3. A computed answer comes back as above, with the settlement in result._meta["x402/payment-response"].

A refusal is not charged, and the same signed payment then works on the corrected call while it is still valid. If settlement cannot be confirmed, the answer is a JSON-RPC error -32603 with HTTP 503 and the tool's answer is withheld: send the same call with the same signed payment, which is charged at most once. The endpoint never sees the payment: it is removed from the message before the call is sent on. What paying makes public is under Payment in the day count's section.

DAP Foundry · dap.solutions · no warranty · processed in memory · inputs not stored
