Unsiloed AI sells to AI agents - live prices and self-serve payment

The agent storefront at https://agents.unsiloed.ai/llms.txt?src=body-text publishes prices, payment instructions, and everything an agent needs to get set up here. Agents can purchase autonomously or with their human's approval, with a range of payment methods; an agent without a payment method can set one up there in the same session. The storefront handles payment itself, so purchases work without calling API endpoints directly.

← Back to Blog
Guides

How to Reconcile Contract Amendments With the Original Agreement

Extract the original agreement and each amendment with Unsiloed, then use Python to apply the changes in effective-date order. The result is the current wording of every clause, with the document and page that set it.

Aman Mishra
Aman Mishra
8 min read
How to Reconcile Contract Amendments With the Original Agreement

An amendment only records what changed. To know what a contract says today, you need the original agreement plus every amendment, applied in the right order. In this guide, we use Unsiloed to extract the original terms and each amendment's changes, then use Python to apply them by effective date. The result is the current wording of each term we track, with the document and page that set it.

The original agreement sits under three clear sheets, one per amendment, laid on in effective-date order. Each sheet covers only the clauses it changes, so the monthly fee reads USD 9,500 after Amendment 1 and USD 10,250 after Amendment 3, Section 5.2 is added and Section 5.1 is struck out. Looking down through the stack shows the terms in force, and each patch is marked with the amendment that set it.

Why the Latest Document Is Not Enough

Our example starts with a hosted software services agreement between Northstar Systems LLC and Harbor Analytics Inc. Three amendments change different parts of it.

Document Signed Effective Changes
Original January 10, 2026 January 15, 2026 Establishes the starting terms
Amendment 1 March 15, 2026 April 1, 2026 Raises the monthly fee and extends the renewal notice period
Amendment 2 June 15, 2026 June 1, 2026 Raises the liability cap and adds a monthly report
Amendment 3 September 10, 2026 October 1, 2026 Raises the fee again and removes the monthly meeting

Amendments rarely restate the contract. They point at numbered clauses, called sections, and say what happens to them. In Amendment 1, the fee change reads as follows.

Section 4.1 of the Agreement is deleted and replaced with the following: "The Customer shall pay the Provider a service fee of USD 9,500 per month."

"Deleted and replaced" means remove the old wording of that section and put the supplied wording in its place. Amendment 3 later targets the same section, including its earlier change.

Section 4.1 of the Agreement, as replaced by Amendment 1, is deleted and replaced with the following: "The Customer shall pay the Provider a service fee of USD 10,250 per month."

An effective date is the date a change starts to apply. It can differ from the signing date. Amendment 2 is signed on June 15 but takes effect on June 1. Store both dates, and order these changes by their effective dates.

Reading only Amendment 3 would miss the 60-day notice period from Amendment 1 and the liability cap from Amendment 2. It would also miss the original renewal and governing law terms, which none of the amendments changes. Keep those links to earlier clauses intact, and preserve section references when splitting a document into smaller pieces for retrieval.

Applying all three amendments gives us the following changes as of October 26, 2026. This is the combined view we'll build, with a source document and page for each term.

Term Original October 26, 2026
Monthly fee USD 8,000 USD 10,250
Notice to prevent renewal 30 days 60 days
Liability cap USD 96,000 USD 150,000
Monthly service meeting Required Deleted
Monthly service report No Section 5.2 Due within five business days after month end

Extract the Original Agreement's Key Terms

Extract a starting set of clauses before processing changes. A schema defines the fields and types we want back. Keep the section number with the complete text of each selected clause, including conditions and units.

Download the original agreement, Amendment 1, Amendment 2, and Amendment 3. Save them in one folder. The snippets use Python with requests installed and an Unsiloed API key in the UNSILOED_API_KEY environment variable. To skip extraction, download the saved results for the original, Amendment 1, Amendment 2, and Amendment 3 into the same folder and go to Put the Changes in Effective-Date Order.

Create extract.py in that folder with the schema below. We select seven clauses covering the terms we will track. The agreement identifier ties the documents together, while the dates distinguish signing from taking effect.

python
def string(description):
    return {"type": "string", "description": description}

def obj(properties):
    return {"type": "object", "properties": properties, "required": list(properties)}

identity = {
    "agreement_id": string("Identifier of the original agreement"),
    "signed_date": string("Date signed, YYYY-MM-DD"),
}
term = obj({
    "section": string("Section number, such as 4.1"),
    "text": string("Complete clause text, verbatim, without its section heading"),
})
original_schema = obj({
    **identity,
    "effective_date": string("Agreement effective date, YYYY-MM-DD"),
    "terms": {"type": "array", "items": term,
              "description": "Sections 2.1, 2.2, 2.3, 4.1, 5.1, 7.1, 9.1 in document order"},
})

Append this request helper to extract.py. The extraction endpoint returns a job ID, and we check the job every few seconds until the result is ready.

python
import json, os, time
import requests

BASE = "https://prod.visionapi.unsiloed.ai"
HEADERS = {"api-key": os.environ["UNSILOED_API_KEY"]}

def extract(path, schema):
    with open(path, "rb") as f:
        job = requests.post(f"{BASE}/v2/extract", headers=HEADERS,
                            files={"pdf_file": (path, f, "application/pdf")},
                            data={"schema_data": json.dumps(schema), "model": "gamma",
                                  "enable_citations": "true"})
    job.raise_for_status()
    while True:
        result = requests.get(f"{BASE}/extract/{job.json()['job_id']}", headers=HEADERS)
        result.raise_for_status()
        result = result.json()
        if result["status"] == "failed":
            raise SystemExit(f"Job failed: {result.get('error')}")
        if result["status"] == "completed":
            return result["result"]
        time.sleep(3)

original = extract("original.pdf", original_schema)
with open("original.json", "w") as f:
    json.dump(original, f, indent=2)

The gamma setting selects the extraction model, and enable_citations adds the page location of each value. Every extracted field comes back with a value, a score, and a citation. The grounding_score says how confident the model is that it found the value in the document, and the extraction_score says how confident it is in the value itself. Both run from zero to one. The citation gives the page and a bounding box, the rectangle on the page where the value appears.

This is the fee clause's text field from original.json.

JSON
{
  "score": {
    "grounding_score": 0.998,
    "extraction_score": 0.998
  },
  "value": "The Customer shall pay the Provider a service fee of USD 8,000 per month.",
  "citation": {
    "bbox": [52, 351, 392, 366],
    "page": 1,
    "page_width": 612.0,
    "page_height": 792.0
  }
}

Extract Each Amendment as a List of Changes

An amendment needs a different schema because it describes edits to existing terms. We extract one record per change, with the target section, the operation, the new text, and the effective date. The date sits on each change because one amendment can contain changes that start at different times.

The recitals, the opening statements that explain the background to an agreement, identify the original contract but don't change anything. Neither does the closing line confirming that all other terms remain unchanged. The schema description tells the model to leave both out.

Amendment 3's text highlighted by role, mapped to two change records. Section 4.1 becomes a replace with the new fee text, Section 5.1 becomes a delete with empty text, and both take effect on 2026-10-01.

Append this schema and loop to extract.py.

python
change = obj({
    "section": string("Target section in the original agreement, not amendment numbering"),
    "operation": {"type": "string", "enum": ["replace", "add", "delete"]},
    "text": string("Complete new clause verbatim, without enclosing quotation marks. Empty string for delete."),
    "effective_date": string("Date this change takes effect, YYYY-MM-DD, not signing date"),
})
amendment_schema = obj({
    **identity,
    "changes": {"type": "array", "items": change,
                "description": "Every operative change, in document order. Exclude recitals and unchanged-terms confirmations."},
})

for n in (1, 2, 3):
    amendment = extract(f"amendment-{n}.pdf", amendment_schema)
    with open(f"amendment-{n}.json", "w") as f:
        json.dump(amendment, f, indent=2)

Run it from the folder containing the PDFs.

Bash
python3 extract.py

In each amendment file, the change records are in ["changes"]["value"], and each field inside a record has its own value, score, and citation, like the original terms. For a deletion, the text is an empty string and the delete operation carries the instruction, so we take page numbers from the operation's citation.

Put the Changes in Effective-Date Order

With every change extracted, we use Python to replay them on top of the original terms. Each section keeps a history of the documents that changed it, so the current wording always points back to its source.

Create reconcile.py and start with the original terms.

python
import json

def load(path):
    with open(path) as f:
        return json.load(f)

def page(field):
    return field["citation"]["page"]

original = load("original.json")
terms = {}
for term in original["terms"]["value"]:
    terms[term["section"]["value"]] = {
        "text": term["text"]["value"],
        "history": [{"document": "original.pdf", "operation": "original",
                     "effective_date": original["effective_date"]["value"],
                     "page": page(term["text"])}],
    }

Append the code that collects every change and sorts it by effective date. The dates are ISO strings (YYYY-MM-DD), so sorting them as text puts them in date order. Each amendment must carry the original's agreement identifier, which stops a change from another contract being applied to this one.

python
changes = []
for n in (1, 2, 3):
    amendment = load(f"amendment-{n}.json")
    if amendment["agreement_id"]["value"] != original["agreement_id"]["value"]:
        raise SystemExit(f"amendment-{n}.pdf belongs to a different agreement")
    for change in amendment["changes"]["value"]:
        changes.append((change["effective_date"]["value"], f"amendment-{n}.pdf", change))
changes.sort(key=lambda item: item[0])

Flag Changes That Need a Person

Some changes can't be applied safely without someone reading the wording. The pipeline should stop and hand the documents to a reviewer, with the page each problem came from, in three cases:

  • Two changes target the same section on the same effective date. Neither document says which one wins, and upload order is not an answer.
  • A value has a low score. The section number, operation, and date matter as much as the new wording, because a wrong section number changes the wrong clause and a wrong date can leave a change out.
  • A replacement or deletion targets a section that is not in force, or an addition targets one that is. The section might be outside the clauses we extracted, or the amendment might point at the wrong number.

Append this check to reconcile.py to catch the first two. It runs on every change, including ones that take effect after the date we reconcile to, so a misread date can't hide a change from review. We check each field's extraction_score against a threshold of 0.9. A deletion's text is an empty string with a low score by design, so we skip it. The third case is checked when each change is applied. The guide to confidence score thresholds explains how to pick a threshold for each field.

python
seen = {}
for effective, document, change in changes:
    section = change["section"]["value"]
    if (effective, section) in seen:
        raise SystemExit(f"{seen[effective, section]} and {document} both change section {section} on {effective}")
    seen[effective, section] = document
    fields = [name for name in change
              if not (name == "text" and change["operation"]["value"] == "delete")]
    if any(change[name]["score"]["extraction_score"] < 0.9 for name in fields):
        raise SystemExit(f"Review the change to section {section} in {document}, page {page(change['operation'])}")

Apply the Changes

Append the last snippet to apply each change that has taken effect by a chosen date. A fixed as_of date keeps the output repeatable. A replacement or deletion needs a section that is currently in force, and an addition needs one that isn't, so we stop when either rule breaks. A deleted section stays in terms with its text set to None, which keeps its removal in the history.

python
as_of = "2026-10-26"
for effective, document, change in changes:
    if effective > as_of:
        continue
    section, operation = change["section"]["value"], change["operation"]["value"]
    if (operation == "add") == bool(terms.get(section, {}).get("text")):
        raise SystemExit(f"{document}, page {page(change['operation'])}: can't {operation} section {section}")
    term = terms.setdefault(section, {"history": []})
    term["text"] = None if operation == "delete" else change["text"]["value"]
    term["history"].append({"document": document, "operation": operation,
                            "effective_date": effective, "page": page(change["operation"])})

with open("current-terms.json", "w") as f:
    json.dump(terms, f, indent=2)
for section, term in sorted(terms.items()):
    print(f"{section} | {term['text'] or '[deleted]'} | {term['history'][-1]['document']}")
print("4.1 history: " + " -> ".join(h["document"] for h in terms["4.1"]["history"]))

Run the full script from the same folder.

Bash
python3 reconcile.py

The script prints the terms in force on October 26, 2026, with the document that last set each one.

text
2.1 | The initial term begins on January 15, 2026 and ends on January 14, 2027. | original.pdf
2.2 | After the initial term, this Agreement renews automatically for successive periods of twelve months. | original.pdf
2.3 | Either party may prevent renewal by giving written notice at least 60 days before the end of the then-current term. | amendment-1.pdf
4.1 | The Customer shall pay the Provider a service fee of USD 10,250 per month. | amendment-3.pdf
5.1 | [deleted] | amendment-3.pdf
5.2 | The Provider shall deliver a written monthly service report to the Customer within five business days after each month ends. | amendment-2.pdf
7.1 | Each party's total liability under this Agreement shall not exceed USD 150,000. | amendment-2.pdf
9.1 | This Agreement is governed by the laws of the State of New York. | original.pdf
4.1 history: original.pdf -> amendment-1.pdf -> amendment-3.pdf

The fee moves from USD 8,000 to USD 9,500, then to USD 10,250 per month. Its history in current-terms.json records each document, operation, effective date, and page, which is the record an extraction audit trail needs. Unchanged terms still point to the original agreement.

This output supports a reviewer rather than replacing legal review.

Continue reading