<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[TableJourney]]></title><description><![CDATA[TableJourney]]></description><link>https://tablejourney.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac4425f0cce94f23095b540/9ff619bd-2e1b-4d9d-a5f8-d04e3aa84437.png</url><title>TableJourney</title><link>https://tablejourney.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 24 Oct 2026 07:44:40 GMT</lastBuildDate><atom:link href="https://tablejourney.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your AI agent can earn a commission on the travel it recommends]]></title><description><![CDATA[Agents already recommend restaurants, food tours and hotels all day. When a person books one of...]]></description><link>https://tablejourney.hashnode.dev/your-ai-agent-can-earn-a-commission-on-the-travel-it-recommends</link><guid isPermaLink="true">https://tablejourney.hashnode.dev/your-ai-agent-can-earn-a-commission-on-the-travel-it-recommends</guid><category><![CDATA[AI]]></category><category><![CDATA[mcp]]></category><category><![CDATA[agents]]></category><category><![CDATA[Travel]]></category><dc:creator><![CDATA[TableJourney]]></dc:creator><pubDate>Wed, 21 Oct 2026 04:08:22 GMT</pubDate><content:encoded><![CDATA[<hr />
<h2>title: Your AI agent can earn a commission on the travel it recommends
published: true
tags: ai, mcp, agents, travel
cover_image: <a href="https://tablejourney.com/og/cards/partners.jpg">https://tablejourney.com/og/cards/partners.jpg</a></h2>
<p>Agents already recommend restaurants, food tours and hotels all day. When a person books one of those, someone usually earns a commission. It is almost never the agent, or the developer who built it.</p>
<p>We run <a href="https://tablejourney.com">TableJourney</a>, a food travel guide with a public <a href="https://tablejourney.com/agents/">MCP server</a> over 224 cities: verified places to eat, food festivals with resolved dates, and bookable food tours, classes and stays from GetYourGuide, Viator, Musement and hotel partners. This week we opened a partner program, and we built it for agents first.</p>
<h2>The deal</h2>
<ul>
<li><strong>50% of the commission we are paid</strong> on bookings made by the people you send. <strong>60%</strong> in any month where your settled earnings pass $500.</li>
<li>Free to join. Payouts monthly by PayPal, USDC on Base or bank transfer, from $50, 30 days after the booking partner pays us.</li>
<li>Our booking partners have approved sub-affiliates, so this is inside their terms, not around them.</li>
</ul>
<p>Why half: the share is of our commission, not of the booking price, so a small percentage of it would be a few cents a booking. Half is enough to be worth an agent's while, and our pages still do part of the selling.</p>
<h2>Joining without a browser</h2>
<p>Everything is MCP tools on <code>https://tablejourney.com/mcp</code> (streamable HTTP, no key needed to read):</p>
<pre><code class="language-json">{"name": "partner_join", "arguments": {"email": "agent-owner@example.com", "accept_terms": true, "kind": "agent", "name": "My travel agent"}}
</code></pre>
<p>A six-digit code goes to that address. Then:</p>
<pre><code class="language-json">{"name": "partner_confirm", "arguments": {"email": "agent-owner@example.com", "code": "123456"}}
</code></pre>
<p>That returns an API key and a referral code. The address belongs to the person or company behind the agent, because a person receives the payouts.</p>
<h2>How a booking gets attributed</h2>
<p>Send the key as <code>Authorization: Bearer &lt;key&gt;</code> on the MCP connection. From then on every booking link the read tools return (<code>experiences</code>, <code>stays</code>, <code>tickets</code>, <code>car_hire</code> and the rest) is a <code>/go/</code> link signed for your key, and the link carries your code in the partner's own sub-id field: <code>cmp</code> for GetYourGuide, <code>campaign</code> for Viator, <code>cid</code> for Musement, <code>clickref</code> for Awin and <code>sid</code> for CJ. That sub-id is what each partner reports back with the commission, so attribution does not depend on a cookie surviving in somebody's browser.</p>
<p>If your agent sends people to pages instead, <code>partner_link</code> returns any tablejourney.com page with <code>?ref=&lt;code&gt;</code>. The site holds the code for 30 days and stamps it into every partner link the visitor sees, including ones that appear after the page loads.</p>
<p><code>partner_stats</code> shows visits, booking clicks, and earnings as pending, confirmed and paid. <code>partner_payout_method</code> sets where the money goes; it takes effect only after a code sent to the account's address, so a leaked key cannot redirect payouts.</p>
<h2>For agents that prefer OAuth</h2>
<p>The usual discovery documents are there:</p>
<ul>
<li><code>/.well-known/oauth-authorization-server</code> (RFC 8414), with dynamic client registration (RFC 7591) and PKCE S256</li>
<li><code>/.well-known/oauth-protected-resource</code> (RFC 9728); a 401 from the partner API carries <code>WWW-Authenticate</code> pointing at it</li>
<li><code>/.well-known/agent-card.json</code>, an A2A card whose food travel skill is answered by our own model from the same database</li>
</ul>
<p>An MCP client that handles the OAuth flow can sign the person in through the browser instead of handling a key.</p>
<h2>What we ask</h2>
<p>Pass booking links on unchanged, tell people they are affiliate links, and point them at the page you got the facts from. The full terms are short: <a href="https://tablejourney.com/partners/terms/">https://tablejourney.com/partners/terms/</a></p>
<p>Program page: <a href="https://tablejourney.com/partners/">https://tablejourney.com/partners/</a>. Agent guide and the full tool list: <a href="https://tablejourney.com/agents/">https://tablejourney.com/agents/</a>. Questions go to <a href="mailto:support@tablejourney.com">support@tablejourney.com</a>, and we read them.</p>
<hr />
<p><em>First published on <a href="https://dev.to/tablejourney/your-ai-agent-can-earn-a-commission-on-the-travel-it-recommends-8do">DEV</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Stop storing event dates: store the rule and compute the next edition]]></title><description><![CDATA[Recurring events drift out of date when you store a date. Store the rule (nth weekday, Easter, lunar anchors) and compute the next edition in Python.]]></description><link>https://tablejourney.hashnode.dev/stop-storing-event-dates-store-the-rule-and-compute-the-next-edition</link><guid isPermaLink="true">https://tablejourney.hashnode.dev/stop-storing-event-dates-store-the-rule-and-compute-the-next-edition</guid><category><![CDATA[Python]]></category><category><![CDATA[datetime]]></category><category><![CDATA[#OpenData]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[TableJourney]]></dc:creator><pubDate>Mon, 19 Oct 2026 07:51:04 GMT</pubDate><content:encoded><![CDATA[<p>Most event listings go stale in a predictable way. Someone types "18 to 21 November 2026" into a database, the event happens, and the page keeps showing last year's dates until a person notices. For one-off events that is fine. For annual events it is a bug you ship every year.</p>
<p>At TableJourney we keep the next dates of about 1,400 food festivals. Typing dates by hand did not survive the first year, so we stopped storing dates and started storing the rule a festival follows. The site computes the next edition from the rule every day. This post shows the shapes of rule we needed, with working Python.</p>
<h2>Three kinds of recurrence</h2>
<p>After going through the whole dataset, almost every annual event fell into one of three groups:</p>
<ol>
<li><strong>Fixed day of the year.</strong> "Every 24 to 26 December." The easy case: store month and day, roll the year forward.</li>
<li><strong>Nth weekday of a month.</strong> "The third Thursday of November, for four days." The weekday holds, the day number moves.</li>
<li><strong>Anchored to a moveable feast.</strong> Easter, Lunar New Year, Mid-Autumn, Ramadan, Diwali. These move by weeks between years, so neither of the first two works.</li>
</ol>
<p>A fourth shape, "the weekend after Lunar New Year", turned out to be common enough to need its own rule, and it is the one that catches people out.</p>
<h2>The rule as data</h2>
<p>Each event carries a small <code>date_rule</code> object instead of a date. These are real rules from our data:</p>
<pre><code class="language-json">{"nth_weekday": [3, "Thursday"], "length": 4}
{"nth_weekday": [-1, "Monday"], "offset": -1, "length": 2}
{"anchor": "easter", "end_offset": -47}
{"anchor": "chinese-new-year", "start_offset": 0, "end_offset": 14}
{"anchor": "chinese-new-year", "weekday_after": "Saturday", "length": 2}
</code></pre>
<p>The second one reads "the Sunday before the last Monday of the month, for two days", which is how a lot of bank-holiday weekend events are actually defined. The month comes from the event's stored <code>start_month</code>; the rule never contains a year, so it keeps working.</p>
<h2>Nth weekday</h2>
<p>The helper is short. <code>n</code> is 1 to 5, or -1 for the last one:</p>
<pre><code class="language-python">import calendar
import datetime as dt

def nth_weekday(year, month, weekday, n):
    """n-th (1..5, or -1 for last) weekday (0=Monday) of the month."""
    days = [
        dt.date(year, month, d)
        for d in range(1, calendar.monthrange(year, month)[1] + 1)
        if dt.date(year, month, d).weekday() == weekday
    ]
    if n == -1:
        return days[-1]
    return days[n - 1] if 1 &lt;= n &lt;= len(days) else None

print(nth_weekday(2026, 11, 3, 3))   # third Thursday of November 2026
print(nth_weekday(2027, 5, 0, -1))   # last Monday of May 2027
</code></pre>
<pre><code class="language-text">2026-11-19
2027-05-31
</code></pre>
<p>Returning <code>None</code> for "the fifth Friday" in a month that has four is deliberate: the caller can then look at the next year instead of inventing a date.</p>
<h2>Easter: compute it, do not table it</h2>
<p>Easter is the one moveable feast you can compute exactly. This is the Anonymous Gregorian algorithm, as it sits in our code:</p>
<pre><code class="language-python">def easter(year):
    """Gregorian Easter Sunday (Anonymous Gregorian algorithm)."""
    a = year % 19
    b, c = divmod(year, 100)
    d, e = divmod(b, 4)
    f = (b + 8) // 25
    g = (b - f + 1) // 3
    h = (19 * a + b - d - g + 15) % 30
    i, k = divmod(c, 4)
    l = (32 + 2 * e + 2 * i - h - k) % 7
    m = (a + 11 * h + 22 * l) // 451
    month, day = divmod(h + l - 7 * m + 114, 31)
    return dt.date(year, month, day + 1)

print([str(easter(y)) for y in (2026, 2027, 2028)])
</code></pre>
<pre><code class="language-text">['2026-04-05', '2027-03-28', '2028-04-16']
</code></pre>
<p>Carnival, Lent markets and anything defined as "N days before Easter" become an offset from this date. The <code>{"anchor": "easter", "end_offset": -47}</code> rule above ends a season on Shrove Tuesday, 47 days before Easter Sunday.</p>
<h2>Lunar and religious calendars: a small table, sourced</h2>
<p>Lunar New Year, Mid-Autumn, Ramadan, Eid and Diwali cannot be computed reliably from a formula you want to maintain. We keep a small JSON table of anchor dates per year, each from a published source (an observatory's lunar tables, published religious calendars), and the code reads it:</p>
<pre><code class="language-json">{"chinese-new-year": {"2026": "2026-02-17", "2027": "2027-02-06", "2028": "2028-01-26"}}
</code></pre>
<p>The table has an end. When an anchor runs out of years, the resolver returns <code>None</code> and the event keeps its stored month and day, and the data validator flags every rule whose anchor table ends too soon, so the table gets extended before it matters. A table that silently ends is worse than no table.</p>
<h2>The weekend-after rule</h2>
<p>Here is the trap. London's Chinatown celebration happens on the first weekend on or after Lunar New Year. In 2026 the holiday is a Tuesday, so the event is the following Saturday. In 2027 the holiday is itself a Saturday. A fixed offset like "+4 days" lands on a Saturday one year and a Wednesday the next, so it cannot express this. Snap forward to a weekday instead:</p>
<pre><code class="language-python">WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

def snap_forward(day, weekday_name):
    target = WEEKDAYS.index(weekday_name)
    return day + dt.timedelta(days=(target - day.weekday()) % 7)

lny_2027 = dt.date(2027, 2, 6)
start = snap_forward(lny_2027, "Saturday")
print(start, start + dt.timedelta(days=1))
</code></pre>
<pre><code class="language-text">2027-02-06 2027-02-07
</code></pre>
<p>We also support <code>weekday_nearest</code> (the closest named weekday in either direction) for events that celebrate a holiday on the weekend nearest to it.</p>
<h2>Picking the next edition</h2>
<p>With an occurrence function per rule, the next edition is the earliest occurrence that has not ended yet. Check a small window of years, because an event in early January belongs to next year when you ask in December, and a season that started last year may still be running:</p>
<pre><code class="language-python">def next_occurrence(rule_occurrence, today):
    best = None
    for year in (today.year - 1, today.year, today.year + 1, today.year + 2):
        occ = rule_occurrence(year)          # (start, end) or None
        if occ and occ[1] &gt;= today:
            if best is None or occ[0] &lt; best[0]:
                best = occ
            break
    return best
</code></pre>
<p>Comparing against the end date rather than the start keeps a running event on the page as "on now" until its last day. Some events run several editions a year (restaurant weeks in winter and summer), so the real code takes a list of rules and picks the soonest across all of them.</p>
<h2>What this bought us</h2>
<ul>
<li>Titles, snippets and structured data always show the next real dates, with no yearly data entry.</li>
<li>One Hong Kong page had been advertising 17 February 2027 for Lunar New Year, when the holiday is on the 6th. A rule cannot make that mistake.</li>
<li>The same resolved dates feed everything downstream: the pages, the calendar feeds, the API and an open dataset. One computation, many outputs, no disagreements between them.</li>
</ul>
<p>The rule format is deliberately boring JSON so that research agents and people can both write it. When a source states a pattern in prose ("the second weekend of September"), we encode the pattern, then check that the rule reproduces the date the organiser published for this year before trusting it.</p>
<p>If you want to see the result, the dates of all of these events are in an open dataset (CC BY 4.0) you can query or download: <a href="https://tablejourney.com/festivals/statistics/">https://tablejourney.com/festivals/statistics/</a> has the summary and the links. How do you handle recurring dates in your own projects? I would like to hear about rule shapes we have not needed yet.</p>
<hr />
<p><em>First published on <a href="https://dev.to/tablejourney/stop-storing-event-dates-store-the-rule-and-compute-the-next-edition-2lgc">DEV</a>.</em></p>
]]></content:encoded></item></channel></rss>