Platform
Documentation
What a receipt is, how to check one with our interface or without us, and what each interface on this site takes and returns. A test runs every example.
This page documents the interfaces that graphatlas.com offers today. They are small, and they serve one purpose, which is to let you check a figure without taking our word for it. The reference for each licensed engine, with its inputs, outputs and schemas, is delivered with the licence.
Every request and answer below is run by a test against the code that serves this site, and so are the Python and the JavaScript. If this page and an interface disagree, a test fails.
What a receipt is
A receipt is a 64-character fingerprint of a calculation. It covers the inputs, the parameters of the rule that was applied, and the result. Two runs have the same receipt if and only if they agree on every one of those numbers, to the last bit.
That is how an engine, a browser, a server and an auditor’s own tools can each say that they reached the same answer. The receipt bench lets you change three cash flows and watch the receipt follow.
How a receipt is made
There are three steps, and none of them needs our software.
- Write the calculation as a value. The value holds the kind of calculation, its inputs, the parameters of the rule and the result. It is made of numbers, text, true and false, null, lists and objects.
- Write the value as one line of text. A number is written as
f64:and the 16 hexadecimal digits of its IEEE 754 double precision bits, so that no language’s number formatting can change it. Text is written as a JSON string. A list keeps its order. The keys of an object are sorted and each is written as a JSON string. There are no spaces. - Hash the text. The receipt is the SHA-256 digest of the text as UTF-8, in lowercase hexadecimal.
For three cash flows of 100, 100 and 1,100 discounted at 4.5 percent, the text is the line below. It has no line breaks, and the box scrolls sideways.
{"inputs":{"cashflows":[f64:4059000000000000,f64:4059000000000000,f64:4091300000000000],"ratePercent":f64:4012000000000000,"years":[f64:3ff0000000000000,f64:4000000000000000,f64:4008000000000000]},"kind":"present-value","result":f64:4091f0524c665c16}Its SHA-256 is:
01cb05ef1a8b46f414173d0a332e117258339dfa6b1b2c48ec9e97bc3c95c5a5Writing numbers as bits is what makes a receipt strict. Zero and minus zero give different receipts, and so does a result that differs in its last place. That is intended, because a figure that is almost the same is not the same figure.
The verification API
POST https:// takes the inputs of a calculation, runs it, and answers with the receipt of the result. If you send a receipt that you were given, it also says whether the two match.
Nothing you send is stored. The only thing recorded is a count for the rate limit, as the privacy notice explains. A GET to the same address describes the interface in JSON.
Request
Send JSON, with the header Content-Type: application/.
| Field | Type | Meaning |
|---|---|---|
kind |
text | Which calculation to run. The kinds are listed below. |
input |
object | What that kind takes. |
receipt |
text, optional | A receipt to compare with, as 64 hexadecimal digits. Capitals are accepted. |
canonical |
true or false, optional | Set to true to get the text that was hashed in the answer. |
Any other field is refused. It is more likely to be a misspelling than a request, and ignoring it would answer a question that you did not ask.
A request for the present value of three cash flows:
{
"kind": "present-value",
"input": { "cashflows": [100, 100, 1100], "ratePercent": 4.5 }
}Response
The answer to that request:
{
"kind": "present-value",
"receipt": "01cb05ef1a8b46f414173d0a332e117258339dfa6b1b2c48ec9e97bc3c95c5a5",
"supplied": null,
"matches": null,
"differingBits": null,
"result": { "presentValue": 1148.0803695672707 }
}| Field | Meaning |
|---|---|
kind |
The kind that was run. |
receipt |
The receipt of the result, which is the one that matters. |
supplied |
The receipt you sent, in lowercase, or null if you sent none. |
matches |
True if the two are the same, false if they are not, and null if you sent none. |
differingBits |
How many of the 256 bits differ, when they do not match. Otherwise null. |
result |
The figures that the calculation gave. The fields depend on the kind. |
canonical |
The text that was hashed. It is there only when you asked for it. |
The header X-GraphAtlas-Build names the build that answered. The body does not carry it, so every build that applies the same rules gives the same body for the same request.
The kinds you can check
| Kind | It takes | It gives |
|---|---|---|
present-value |
Three yearly cash flows and a rate in percent. | The present value. |
frtb-sa/ |
A reporting currency and one bucket for each currency, each with ten PV01 values in the order of the tenors, 0.25 years to 30 years. | The capital charge, the scenario that binds and the charge of each of the three. See FRTB-SA. |
solvency-ii/ |
The five module charges, the operational risk charge and the loss-absorbing adjustment. | The basic and the final solvency capital requirement. See Solvency II. |
priips/ |
The standard deviation, skewness and excess kurtosis of the daily log returns, the holding period in years and the number of periods in a year. | The value at risk, its equivalent volatility and the market risk class. See PRIIPs. |
entropy/ |
The claimed min-entropy for each sample, and the samples as lowercase hexadecimal. | Both continuous health tests, the first alarm and the most common value estimate. See Entropy validation. |
pqc/ |
A planning year, the years until a quantum computer, a framework, and from 1 to 64 uses. Each use has an id, an algorithm, a purpose, the years to keep it secure, the years to migrate, an impact, the ids it waits for and a replacement. | The year each migration must start and finish, in the order they must start, with how many are late. See Post-quantum migration. |
aermod/plume |
The height of a stack, the temperature of the gas, the wind at ten metres and a stability class from A to F. | The wind at the top of the stack, the two fluxes, where the rise stops and the highest concentration at the ground with the distance it falls at. See AERMOD. |
aermod/ |
One of four standards, a stack height, a distance and a bearing for a receptor, and what is already in the air. | The value of each year, the design value of each window of three years as a monitor keeps it, the same hours as a model is compared, and the highest hour. See AERMOD. |
reserving/ |
A basis, cumulative for running totals or incremental for the amounts of each year, and a triangle of 2 to 40 origin years with the oldest first. |
The development factors, the ultimate and the reserve of each origin year, what the factors expect to be paid in each year to come, and the standard error of Mack, or the reason that it cannot be given. See Reserving and ESG. |
reserving/curve |
The kind of the rates, par or zero, from 1 to 30 maturities of 1 to 150 whole years with a rate for each, the ultimate forward rate, a valuation day, the currency, whether the phasing in applies, the first smoothing point and a rule. |
The rule that applied and whether the day or you chose it, alpha, and the annual zero rate at 13 chosen years. The receipt covers all 150 years. See Reserving and ESG. |
sa-cva/capital |
A reporting currency and from 1 to 200 positions. A position has a risk, a bucket, a factor, the sensitivity of the CVA and the sensitivity of its hedges. A credit spread adds an entity, a quality and a group. | The capital and the risk-weighted assets, the delta and the vega capital, the capital with no hedge and what the hedges save, and the capital of each class. The receipt covers every bucket. See SA-CVA. |
ics/capital |
The 18 charges of the group, the premiums and liabilities that operational risk is worked from, the tax rate and rule, the charge for non-insurance entities and, if wanted, the capital resources. | The requirement, the insurance risk charge, the charge of each category, the aggregate, operational risk, the tax effect, the row of the spread, and the capital that counts with what is left out and the cover. The receipt covers every step. See ICS. |
simm/margin |
From 1 to 400 lines of a risk data file. A line has a product class, a risk type, a qualifier and an amount, and may have a trade, a bucket and two labels. | The margin, the margin with no concentration and with no diversification, the margin of each product class and of each risk class in it, the number of risk factors, and the share of each trade. The receipt covers every bucket and every factor. See SIMM. |
simm/reconcile |
Two files of the same layout, ours and theirs, each of 1 to 400 lines. | Both margins, the difference, whether the two agree to half a cent, the margin of each product class on each side, and each risk factor that differs, with a guess at why, the two amounts and what the factor is worth. The receipt covers both calculations and every difference. See SIMM. |
The two AERMOD kinds run a textbook plume and five years of weather that was made from a seed. Nobody measured it, and the engine behind these kinds is not AERMOD. A request for aermod/ runs five years of hourly records, which takes a few tens of milliseconds. A wind below 0.5 metres a second is read as 0.5. The gas must be at least 290 kelvin, so that it is warmer than the 15 degrees of the air, because the formulas for the limit of the rise describe a buoyant plume.
The two Reserving kinds run the published methods on the triangle or the rates that you send. A triangle has each row one amount shorter than the row before it, and a request that is not a triangle is refused with the row that is wrong. The standard error of Mack needs at least four origin years and is null for fewer, with standardErrorWhyNot saying so. A reserving/curve request with rule set to date follows the framework in force on the valuation day, which changes on 30 January 2027. A request for one of the other two rules says so in decidedBy. The search for alpha takes a few milliseconds for rates up to 30 years, and up to about four tenths of a second when the maturities reach 150 years.
A request for sa-cva/capital carries sensitivities and not prices. The risks are ir-delta, fx-delta, cs-delta, ir-vega and fx-vega. A bucket is a currency, or for a credit spread one of the sectors 1a, 1b and 2 to 7. A factor is a tenor such as 5y, or spot or volatility for FX, or rates or inflation for the volatility of rates. A sensitivity is for one basis point of a rate or a spread and for one per cent of an exchange rate or a volatility, and a hedge that offsets has the sign of the CVA that it is set against. Positions that name the same line are added before anything else. A book that the rule cannot take is refused as out_of_domain with the position that is wrong, counting from one. A sensitivity of -0 is read as 0, because the receipt writes no minus zero. The kind works out the rule of July 2020 for five of the eleven classes, and the page of the product says which.
A request for ics/capital carries the charges that the models of the group have worked out and not the stresses. A charge is the fall in capital resources under a stress, so it is zero or more. There are five life charges, one for non-life, four for catastrophe, seven for market risk, where the spread has a charge for the stress up and one for the stress down, and one for credit. The operational exposures are the premiums of the year and of the year before and the gross current estimate for non-life and for life with risk, and the estimate for life without risk. A tax.limit of null takes the default rule, 80% of the notional tax effect. Otherwise it carries up to 50 sub-groups, the earnings, whether a loss is projected and the deferred tax. A resources of null leaves the capital out. A group that the rule cannot take is refused as out_of_domain in the words of the engine. An amount of -0 is read as 0, because the receipt writes no minus zero. The cover is null when the requirement is zero, because a ratio with nothing under it is not a number. The kind takes the amounts in one unit of your choice, up to 1,000,000,000,000. A requirement above that limit is refused when capital resources are set against it.
The two SIMM kinds run the structure of the ISDA methodology on a calibration of our own, called sketch-1, because ISDA licenses its tables. The margin is not a SIMM margin, and the receipt names the calibration. A line is a row of a risk data file with its amount in US dollars, for one basis point of a rate or a credit spread and for one per cent of an equity, a commodity or an FX rate. A rate has a currency as its qualifier, a tenor such as 5y in label1 and a sub-curve such as OIS in label2. A credit spread has an issuer, a bucket and a vertex in label1, and an equity or a commodity has a name and a bucket. A trade, bucket, label1 or label2 that the layout leaves unused may be left out. Lines that name the same risk factor are added before anything else, and an FX line in US dollars is left out and counted in ignored. A file that the rule cannot take is refused as out_of_domain in the words of the engine, with the line that is wrong, counting from one, and simm/reconcile says whether it is ours or theirs. A line takes an amount up to 1,000,000,000,000 in size, and a file takes at most 400 lines. The closes of a difference is what our margin falls by when that line alone is made the same as theirs, so it is below zero where the other side has the larger figure, and the figures do not add up to the difference.
A plan takes at most 64 uses. A use has no name, because the receipt covers the id and not the name, and a request that carries one is refused like any other key that the kind does not know. A use that waits for others lists their ids in after, and one that has no replacement of its choice leaves replacement null.
A list of PV01 values is a list, in the order of the tenors. A JavaScript object with keys such as 1 and 0.25 does not keep them in the order they were written, so a request that used one would mean different things to different languages.
Compare with a receipt you were given
Add the receipt to the request. Here the receipt was made for a final cash flow of 1,100.01, and the request says 1,100:
{
"kind": "present-value",
"input": { "cashflows": [100, 100, 1100], "ratePercent": 4.5 },
"receipt": "8281681d04b566ce2c9031ba13460d949dcbcd3ae3306c918fec6006d6fd83ca"
}{
"kind": "present-value",
"receipt": "01cb05ef1a8b46f414173d0a332e117258339dfa6b1b2c48ec9e97bc3c95c5a5",
"supplied": "8281681d04b566ce2c9031ba13460d949dcbcd3ae3306c918fec6006d6fd83ca",
"matches": false,
"differingBits": 116,
"result": { "presentValue": 1148.0803695672707 }
}Two unrelated receipts differ in about half of their 256 bits, and 116 is what a different calculation looks like. One mistyped digit changes at most four bits, so a very small count points to a slip of the keyboard and not to a different calculation.
Ask for the text that was hashed
Set canonical to true and the answer carries the text of the second step.
{
"kind": "present-value",
"input": { "cashflows": [100, 100, 1100], "ratePercent": 4.5 },
"canonical": true
}{
"kind": "present-value",
"receipt": "01cb05ef1a8b46f414173d0a332e117258339dfa6b1b2c48ec9e97bc3c95c5a5",
"supplied": null,
"matches": null,
"differingBits": null,
"result": { "presentValue": 1148.0803695672707 },
"canonical": "{\"inputs\":{\"cashflows\":[f64:4059000000000000,f64:4059000000000000,f64:4091300000000000],\"ratePercent\":f64:4012000000000000,\"years\":[f64:3ff0000000000000,f64:4000000000000000,f64:4008000000000000]},\"kind\":\"present-value\",\"result\":f64:4091f0524c665c16}"
}The receipt is the SHA-256 of that text, which any tool can check. The next section does it.
Errors
An error is a JSON object with an error in it. The error has a code, a message and, where it helps, issues that say where in the request the problem is, or kinds that lists what exists. This request has two cash flows where the kind needs three:
{
"kind": "present-value",
"input": { "cashflows": [100, 100], "ratePercent": 4.5 }
}The answer has the status 422 and this body:
{
"error": {
"code": "invalid_input",
"message": "The input is not valid for this kind.",
"issues": [
{ "path": "input.cashflows", "message": "Give exactly three cash flows, for years 1, 2 and 3." }
]
}
}An unknown kind is refused with the list of kinds that do exist:
{ "kind": "frtb", "input": {} }{
"error": {
"code": "unknown_kind",
"message": "There is no kind \"frtb\".",
"kinds": [
"present-value",
"frtb-sa/girr-delta",
"solvency-ii/standard-formula",
"priips/market-risk",
"entropy/health-run",
"pqc/migration-plan",
"aermod/plume",
"aermod/design-value",
"reserving/chain-ladder",
"reserving/curve",
"sa-cva/capital",
"ics/capital",
"simm/margin",
"simm/reconcile"
]
}
}These are all the codes, with the status that goes with each.
| Status | code |
When |
|---|---|---|
| 400 | invalid_json |
The body is not JSON, is not UTF-8, or starts with a byte order mark. |
| 400 | invalid_request |
The body is JSON but not an object with a kind and an input, it has another field, or the receipt is not 64 hexadecimal digits. |
| 405 | method_not_allowed |
The method is not GET, HEAD or POST. The header Allow lists them. |
| 413 | too_large |
The body is larger than the limit below. |
| 415 | unsupported_media_type |
The Content-Type is not JSON. |
| 422 | unknown_kind |
There is no such kind. |
| 422 | invalid_input |
The input does not fit the kind. Each problem is in issues, with its path. |
| 422 | out_of_domain |
The input fits, but lies outside the range the rule is defined for. The message says why. |
| 429 | rate_limited |
Too many requests in a minute. The header Retry-After says how many seconds to wait. |
Limits
- A request may be at most 256 KiB.
- A stream of samples may hold at most 100,000 samples, which is 200,000 hexadecimal digits.
- A network may send 60 requests a minute. An answer to a request that was counted carries a
RateLimitheader with the limit, what remains and the seconds until the count resets. A request refused for its content type or its method costs nothing and carries none. - A book may have at most 40 currencies, and each figure has a bound that the message names if you cross it.
If you need to check thousands of results, write to us. We would sooner deliver the engine to run next to your data.
Reproduce a receipt without us
Anyone can repeat the last two steps of a receipt, from an answer, with tools that they already trust. That does not check our rules. It checks that the receipt belongs to the numbers in front of you, and that nobody has changed them since it was made.
With a few lines of Python
Save the answer to the first request:
curl --silent https://graphatlas.com/api/verify \
--header 'content-type: application/json' \
--data '{"kind":"present-value","input":{"cashflows":[100,100,1100],"ratePercent":4.5}}' \
> response.jsonThis script builds the text for that calculation from what you sent and what came back, hashes it, and compares the result with the receipt in the answer. It uses the standard library only.
import hashlib
import json
import struct
def canonical(value):
"""The text that a receipt hashes, for numbers, text, lists and objects."""
if isinstance(value, bool):
return "true" if value else "false"
if value is None:
return "null"
if isinstance(value, (int, float)):
return "f64:" + struct.pack(">d", value).hex()
if isinstance(value, str):
return json.dumps(value, ensure_ascii=False)
if isinstance(value, list):
return "[" + ",".join(canonical(item) for item in value) + "]"
members = (json.dumps(key, ensure_ascii=False) + ":" + canonical(value[key]) for key in sorted(value))
return "{" + ",".join(members) + "}"
request = {"cashflows": [100, 100, 1100], "ratePercent": 4.5}
answer = json.load(open("response.json"))
payload = {
"kind": "present-value",
"inputs": {**request, "years": [1, 2, 3]},
"result": answer["result"]["presentValue"],
}
receipt = hashlib.sha256(canonical(payload).encode("utf-8")).hexdigest()
print(receipt == answer["receipt"], receipt)It prints True and the receipt. The function canonical is the whole of the second step, for any value made of numbers, text, lists and objects, so it serves the other kinds too. Ask for canonical to see the layout of the value that a kind hashes.
From a browser or Node
This asks the server to compare a receipt, which is what you would do with one that a counterparty sent. It runs in a browser console and in Node 18 or later.
const request = {
kind: 'present-value',
input: { cashflows: [100, 100, 1100], ratePercent: 4.5 },
receipt: '01cb05ef1a8b46f414173d0a332e117258339dfa6b1b2c48ec9e97bc3c95c5a5',
};
const response = await fetch('https://graphatlas.com/api/verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request),
});
const answer = await response.json();
console.log(answer.matches, answer.result.presentValue);It prints true 1148.0803695672707.
What a match tells you
A match says that the inputs you sent, under the rules that our kernel applies today, give a result with exactly these bits. It does not say that the rules are the regulation. That is the subject of the verification page, which lists what is proved, what is tested against published figures, and what is read from the text and nothing more.
The check runs the rules of the build that answers. If a rule changed after a receipt was made, the receipt no longer matches, and that is the right answer, because the figure it covers was made under another rule. The changelog says when each rule changed.
Parity data
GET / returns the result of the last parity run. It lists the fixtures, which are calculations with fixed inputs, the engines that ran them, the receipt that each engine produced for each fixture, and one combined receipt over all of them. The platform page shows the same file as a table.
The field agree is true when every engine produced the same receipt for every fixture. The field platformMath counts, for each engine and each of the mathematical functions that come with the platform, the points of a fixed grid at which the engine gives a different answer from Node. Its widestGap field gives, for the same engines and functions, the largest distance between those answers and Node’s, in units in the last place. Those counts are why the kernel does not use the functions that come with the platform.
The specification feed
GET / is an Atom feed of what the standard setters published and which release of ours answers it. Each entry names the standard setter as its author, has a category for each product that it concerns and one for its kind, and says in its summary when it takes effect and which release answers it, or that we are still tracking it. The updated time of an entry is the date of its release, if it has one. Its identifier is built from the publication date and a fixed name, so it does not change when a title does.
The feed is fresh for five minutes and may be served stale for an hour while a new copy is fetched. It sends an ETag. Send it back in If-None-Match and an unchanged feed answers 304 with no body. The page at spec feed shows the same events, with filters.
The evidence bundle
GET / redirects to the current evidence bundle. It is a tar file that holds the proof files, what the solvers answered, a list of SHA-256 hashes and a script that checks them. The bundle section of the verification page shows the name, the size and the hash of the current one.
The name of the bundle carries the first digits of a hash of its contents, so the file at a name never changes. Only the current bundle is served. An older name answers 404, and the address above leads to the new one.
Every answer carries an ETag, which is the SHA-256 of the file in quotes, and a Repr-Digest header that gives the same hash in the form of RFC 9530. A client can compare what arrived with what the page says. The server takes one range of bytes in a Range header, and it takes If-Range and If-None-Match. A broken download resumes with curl -C - -O, and a copy that you hold is not sent again.
The script verify.sh needs sh and a tool for SHA-256. It checks every file against the list. If z3 or cvc5 is installed, it runs each proof again and counts the answers that say unsat. It says plainly that the proofs were not run when neither is there.
Stability
- The address, the request fields and the answer fields of the verification API do not change in a way that breaks a request that works today. A new optional request field, or a new field in an answer, can appear. Read the fields you need and ignore the rest.
- The names of the kinds do not change. A change to a rule changes receipts and not kinds.
- A rule changes only with a change to the kernel, and each change has an entry in the changelog that names the kinds it affects and says why.